<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Small Dog Energy</title><author><name>Small Dog Energy</name></author><id>https://teletype.in/atom/smalldogenergy</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/smalldogenergy?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/smalldogenergy?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-11T16:48:20.477Z</updated><entry><id>smalldogenergy:keyboard-shortcuts</id><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy/keyboard-shortcuts?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><title>Keyboard Shortcuts for the Rest of the World</title><published>2024-03-06T23:50:09.925Z</published><updated>2024-03-07T10:49:33.398Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/97/99/97990209-43de-47c6-a9a7-1b9e2feff5d8.png"></media:thumbnail><category term="in-english" label="In English"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/94/0f/940fe984-3489-4006-88ea-75ffccd4c085.jpeg&quot;&gt;How the “rest of the world” deals with keyboard shortcuts is often a blind spot for developers used to Latin scripts.</summary><content type="html">
  &lt;p id=&quot;oeWz&quot;&gt;If you’re a native English speaker, you probably don’t think too much about the special role that the English language plays in computer infrastructure. Programming languages use English keywords, domain names use Latin letters, terminal command names are based on English words...&lt;/p&gt;
  &lt;p id=&quot;mIAH&quot;&gt;If you don’t have a way to type in Latin characters, you’ll have a hard time using a computer at all. At the same time, there are many languages out there with alphabets that aren’t based on Latin. How the “rest of the world” deals with this is often a blind spot for developers used to Latin scripts. And sometimes this leads to a worse user experience.&lt;/p&gt;
  &lt;p id=&quot;NQsi&quot;&gt;Mostly, I want this article to cover a specific problem that comes up a lot when handling keyboard shortcuts in web applications: handling non-QWERTY layouts. So I’m going to put a snippet with a proper solution right here:&lt;/p&gt;
  &lt;pre id=&quot;HJOo&quot; data-lang=&quot;javascript&quot;&gt;const actions = {
  &amp;quot;alt + shift + e&amp;quot;: (event) =&amp;gt; {},
  // any other keyboard shortcuts...
};

window.addEventListener(&amp;quot;keydown&amp;quot;, (event) =&amp;gt; {
  const modifiers = [&amp;quot;meta&amp;quot;, &amp;quot;ctrl&amp;quot;, &amp;quot;alt&amp;quot;, &amp;quot;shift&amp;quot;].filter(
    (key) =&amp;gt; event[&amp;#x60;${key}Key&amp;#x60;]
  );

  let key = event.key.toLowerCase();

  if (
    /^\p{Ll}$/u.test(key) &amp;amp;&amp;amp;
    !/^[a-z]$/.test(key) &amp;amp;&amp;amp;
    /^Key[A-Z]$/.test(event.code)
  ) {
    key = event.code.at(3).toLowerCase();
  }

  const hotkey = [...modifiers, key].join(&amp;quot; + &amp;quot;);
  const action = actions[hotkey];

  if (action) {
    event.preventDefault();
    action(event);
  }
});
&lt;/pre&gt;
  &lt;p id=&quot;fMSR&quot;&gt;Now that that’s out of the way, if you want to dive a little bit deeper, let’s start with the basics.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;gdCv&quot;&gt;&lt;strong&gt;Disclaimer.&lt;/strong&gt; Although I use two (and sometimes three) languages in my daily life, they all use simple phonetic LTR scripts, don’t use diacritics, don’t require input method editors — so I probably have my own blind spots!&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;localized-keyboards&quot;&gt;Localized Keyboards&lt;/h2&gt;
  &lt;p id=&quot;sPAD&quot;&gt;Your keyboard probably has keys with letters and symbols printed on them (oh yes, we’re starting with the &lt;em&gt;very&lt;/em&gt; basics). When you press a key, the keyboard sends a numeric code that corresponds to the key you pressed — it’s called a &lt;em&gt;scan code&lt;/em&gt;. The scan codes are assigned to each key by the keyboard hardware itself.&lt;/p&gt;
  &lt;figure id=&quot;sKmM&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/29/ef/29ef0fa1-5e0a-41ce-8fb4-47a185fcfabf.png&quot; width=&quot;2560&quot; /&gt;
    &lt;figcaption&gt;Scan codes on a typical keyboard.&lt;br /&gt;Here and throughout the article keyboard layout diagrams are provided by the brilliant &lt;a href=&quot;https://kbdlayout.info/&quot; target=&quot;_blank&quot;&gt;kbdlayout.info&lt;/a&gt;.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;oLDw&quot;&gt;You’ll find that the position of the control keys like &lt;code&gt;Ctrl&lt;/code&gt;, &lt;code&gt;Alt&lt;/code&gt;, and &lt;code&gt;Enter&lt;/code&gt; doesn’t vary much from country to country, but other keys may have different letters printed on them. In the US, the first six letters on the keyboard spell QWERTY, but in France they sometimes spell AZERTY, or in Germany QWERTZ.&lt;/p&gt;
  &lt;figure id=&quot;WdqH&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f3/8d/f38d8d7c-0a0b-44b6-bcfa-0dec79dd803d.jpeg&quot; width=&quot;966&quot; /&gt;
    &lt;figcaption&gt;Keyboard layouts come in all shapes and colors: AZERTY, QWERTZ...&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;7Ix0&quot;&gt;The Russian alphabet is completely different, with the first six letters spelling ЙЦУКЕН:&lt;/p&gt;
  &lt;figure id=&quot;TQR9&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/5e/08/5e08c172-7b1e-4d5d-81ae-a08140324293.png&quot; width=&quot;2560&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;PZON&quot;&gt;However, the physical keyboard doesn’t care about the symbols printed on its keycaps. Pressing &lt;code&gt;Q&lt;/code&gt; on a US QWERTY keyboard, &lt;code&gt;A&lt;/code&gt; on a French AZERTY keyboard, or &lt;code&gt;Й&lt;/code&gt; on a Russian ЙЦУКЕН keyboard sends out the same scan code. It’s up to the operating system to interpret it correctly.&lt;/p&gt;
  &lt;h2 id=&quot;keyboard-layouts&quot;&gt;Keyboard Layouts&lt;/h2&gt;
  &lt;p id=&quot;g0OG&quot;&gt;That’s why operating systems support different &lt;em&gt;keyboard layouts&lt;/em&gt; — mappings between the scan codes and the actual keys.&lt;/p&gt;
  &lt;p id=&quot;P54H&quot;&gt;So if you have a QWERTZ keyboard, you can tell your operating system that you want to use the QWERTZ layout, and then there will be no mismatch between the keys you press on the keyboard and the characters you see on the screen.&lt;/p&gt;
  &lt;p id=&quot;pHSC&quot;&gt;Things get a little trickier if your alphabet is not Latin-based. If you want to be able to type both your local non-Latin characters &lt;em&gt;and&lt;/em&gt; regular Latin, you’ll have to use two keyboard layouts.&lt;/p&gt;
  &lt;p id=&quot;yLtn&quot;&gt;For the most of my life, my keyboard looked like this:&lt;/p&gt;
  &lt;figure id=&quot;2uUf&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/94/0f/940fe984-3489-4006-88ea-75ffccd4c085.jpeg&quot; width=&quot;1200&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;X9s8&quot;&gt;Notice how it has two sets of characters printed on each key. One set is the regular QWERTY layout, and the second one is the Russian ЙЦУКЕН layout. I’ve never seen a dual-layout keyboard where one of the layouts was not QWERTY.&lt;/p&gt;
  &lt;p id=&quot;LUoh&quot;&gt;When you add two keyboard layouts in your operating system settings, an additional icon appears that shows which layout is currently active. You can click it (or better yet, use a special keyboard shortcut, like &lt;code&gt;Win + Space&lt;/code&gt;) to switch layouts.&lt;/p&gt;
  &lt;figure id=&quot;JKX7&quot; class=&quot;m_retina&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/90/06/90062ab7-5bb0-4667-b6dd-b0412ad2541d.png&quot; width=&quot;497&quot; /&gt;
    &lt;figcaption&gt;Layout switcher on Windows&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;8biX&quot;&gt;The important things to remember are:&lt;/p&gt;
  &lt;ol id=&quot;V3e4&quot;&gt;
    &lt;li id=&quot;9JPz&quot;&gt;Each key has at least two codes: a software-level code that depends on the current layout, and a hardware-level code that does not.&lt;/li&gt;
    &lt;li id=&quot;UwJb&quot;&gt;Even for Latin-based scripts, the default keyboard layout is not always QWERTY.&lt;/li&gt;
    &lt;li id=&quot;0Iih&quot;&gt;For the rest of the world, users usually have a dual layout setup, with QWERTY as the second layout.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2 id=&quot;keyboard-shortcuts&quot;&gt;Keyboard Shortcuts&lt;/h2&gt;
  &lt;p id=&quot;Kub0&quot;&gt;Keyboard shortcuts are yet another thing deeply rooted in the English language. All the standard shortcuts, such as copy (&lt;code&gt;Ctrl + C&lt;/code&gt;), paste (&lt;code&gt;Ctrl + V&lt;/code&gt;), and undo (&lt;code&gt;Ctrl + Z&lt;/code&gt;), were designed with the US QWERTY keyboard in mind.&lt;/p&gt;
  &lt;p id=&quot;8nTf&quot;&gt;Because shortcuts are embedded in our physical memory, we should be very careful about changing them. For this reason, keyboard shortcuts should not be a subject to internationalization. Even if I change the UI language of my operating system to Russian, all shortcuts will still use Latin characters:&lt;/p&gt;
  &lt;figure id=&quot;kISJ&quot; class=&quot;m_retina&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/02/82/0282461b-34b7-4fbb-b17c-f61020fb112f.png&quot; width=&quot;433&quot; /&gt;
    &lt;figcaption&gt;Even though the rest of the UI is in Russian, keyboard shortcuts still refer to the Latin letters.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;pmr7&quot;&gt;And since almost every keyboard has Latin characters (whether it’s just QWERTY, or QWERTY alongside some non-Latin layout), even if I type in Russian using the ЙЦУКЕН layout, I should still be able to look down at my keyboard, find the key with the letter &lt;code&gt;Z&lt;/code&gt; printed on it, and press &lt;code&gt;Ctrl + Z&lt;/code&gt; (which is actually &lt;code&gt;Ctrl + Я&lt;/code&gt; in ЙЦУКЕН) to undo. I shouldn’t have to remember my current layout in order to invoke a shortcut.&lt;/p&gt;
  &lt;figure id=&quot;3KAC&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/46/4a/464a6635-0f80-4be2-b2ec-6b4026113664.jpeg&quot; width=&quot;438&quot; /&gt;
    &lt;figcaption&gt;This should still work as undo, no matter what the current layout is.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;mLkP&quot;&gt;But that doesn’t mean we should rely solely on layout-independent codes (like scan codes) for keyboard shortcuts!&lt;/p&gt;
  &lt;p id=&quot;EDQM&quot;&gt;If I’m using a non-QWERTY Latin layout, it would be extremely confusing to me if the undo shortcut wasn’t &lt;code&gt;Ctrl + Z&lt;/code&gt;, even though &lt;code&gt;Z&lt;/code&gt; might be in an “unusual” place on my keyboard. Pressing &lt;code&gt;Ctrl + Z&lt;/code&gt; with the &lt;code&gt;Z&lt;/code&gt; in the top row of the AZERTY keyboard should also work as undo.&lt;/p&gt;
  &lt;p id=&quot;F7WQ&quot;&gt;So there are really two cases:&lt;/p&gt;
  &lt;ol id=&quot;Tdla&quot;&gt;
    &lt;li id=&quot;GNei&quot;&gt;Either we are using a Latin keyboard layout (not necessarily QWERTY): then we should rely on layout-dependent code to figure out exactly which key was pressed;&lt;/li&gt;
    &lt;li id=&quot;31JU&quot;&gt;Or we are using a non-Latin layout: then it makes sense to assume that QWERTY is the secondary layout, and we should rely on layout-independent code to figure out which key it &lt;em&gt;would have been&lt;/em&gt; in QWERTY.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2 id=&quot;browser-keyboard-events&quot;&gt;Browser Keyboard Events&lt;/h2&gt;
  &lt;p id=&quot;oWue&quot;&gt;This sounds complicated, but the actual implementation is quite simple.&lt;/p&gt;
  &lt;p id=&quot;YzTV&quot;&gt;For keyboard events, the browser provides two event properties:&lt;/p&gt;
  &lt;p id=&quot;NKmi&quot;&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code&quot; target=&quot;_blank&quot;&gt;event.code&lt;/a&gt;&lt;/strong&gt; is layout-independent. It is not a scan code per se, but rather a normalized name based on the scan code of the key: &lt;code&gt;&amp;quot;ControlLeft&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;ShiftRight&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;Numpad0&amp;quot;&lt;/code&gt;, and so on.&lt;/p&gt;
  &lt;p id=&quot;ODMr&quot;&gt;The convenient, if slightly confusing, thing is that for letter keys, &lt;code&gt;event.code&lt;/code&gt; has a name derived from the QWERTY layout: &lt;code&gt;&amp;quot;KeyQ&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;KeyW&amp;quot;&lt;/code&gt;, and so on. And since this code is layout-independent, pressing the &lt;code&gt;Й&lt;/code&gt; key on the ЙЦУКЕН keyboard still results in &lt;code&gt;event.code&lt;/code&gt; equal to &lt;code&gt;&amp;quot;KeyQ&amp;quot;&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;yBHE&quot;&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key&quot; target=&quot;_blank&quot;&gt;event.key&lt;/a&gt;&lt;/strong&gt; is layout-dependent. If the key pressed is a printable character, &lt;code&gt;event.key&lt;/code&gt; is the character that would normally have been printed by pressing that key. Otherwise, it contains one of the predefined values, such as &lt;code&gt;&amp;quot;Control&amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;Shift&amp;quot;&lt;/code&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;
  &lt;p id=&quot;GnGJ&quot;&gt;And now we are ready to revisit the implementation from the beginning of this article:&lt;/p&gt;
  &lt;pre id=&quot;pmDW&quot; data-lang=&quot;javascript&quot;&gt;const actions = {
  &amp;quot;alt + shift + e&amp;quot;: (event) =&amp;gt; {},
  // any other keyboard shortcuts...
};

window.addEventListener(&amp;quot;keydown&amp;quot;, (event) =&amp;gt; {
  const modifiers = [&amp;quot;meta&amp;quot;, &amp;quot;ctrl&amp;quot;, &amp;quot;alt&amp;quot;, &amp;quot;shift&amp;quot;].filter(
    (key) =&amp;gt; event[&amp;#x60;${key}Key&amp;#x60;]
  );

  let key = event.key.toLowerCase();

  if (
    /^\p{Ll}$/u.test(key) &amp;amp;&amp;amp;
    !/^[a-z]$/.test(key) &amp;amp;&amp;amp;
    /^Key[A-Z]$/.test(event.code)
  ) {
    key = event.code.at(3).toLowerCase();
  }

  const hotkey = [...modifiers, key].join(&amp;quot; + &amp;quot;);
  const action = actions[hotkey];

  if (action) {
    event.preventDefault();
    action(event);
  }
});
&lt;/pre&gt;
  &lt;p id=&quot;ALpN&quot;&gt;First, the event handler looks at the layout-dependent &lt;code&gt;event.key&lt;/code&gt;. If it is a Latin letter, or any other non-letter symbol, we use it as is. This covers the case of a user with a Latin layout, and since we rely on layout-dependent codes, non-QWERTY layouts are covered as well.&lt;/p&gt;
  &lt;pre id=&quot;B9xj&quot; data-lang=&quot;javascript&quot;&gt;let key = event.key.toLowerCase();&lt;/pre&gt;
  &lt;p id=&quot;f4dh&quot;&gt;However, if it is a non-Latin character (&lt;code&gt;\p{Ll}&lt;/code&gt; but not &lt;code&gt;[a-z]&lt;/code&gt;), the event handler looks at the layout-independent &lt;code&gt;event.code&lt;/code&gt;. There’s a good chance that in this case &lt;code&gt;event.code&lt;/code&gt; is &lt;code&gt;Key*&lt;/code&gt;, where &lt;code&gt;*&lt;/code&gt; is a Latin letter that occupies the same key in the QWERTY layout (e.g. &lt;code&gt;KeyQ&lt;/code&gt;). After all, non-Latin letters usually occupy the same keys as Latin letters do in QWERTY.&lt;/p&gt;
  &lt;p id=&quot;NdN6&quot;&gt;In this case, all we have to do is to trim the &lt;code&gt;Key&lt;/code&gt; prefix to get the Latin letter. This is a very simple (but slightly hacky) way to map non-Latin letters to the letters of the QWERTY layout.&lt;/p&gt;
  &lt;pre id=&quot;rid3&quot; data-lang=&quot;javascript&quot;&gt;if (
  /^\p{Ll}$/u.test(key) &amp;amp;&amp;amp;
  !/^[a-z]$/.test(key) &amp;amp;&amp;amp;
  /^Key[A-Z]$/.test(event.code)
) {
  key = event.code.at(3).toLowerCase();
}&lt;/pre&gt;
  &lt;p id=&quot;6xQw&quot;&gt;(I first came across this method in the &lt;a href=&quot;https://github.com/ai/keyux&quot; target=&quot;_blank&quot;&gt;KeyUX&lt;/a&gt; library. Its clever and compact implementation inspired this whole article.)&lt;/p&gt;
  &lt;p id=&quot;IvGt&quot;&gt;Of course, this implementation is not perfect. For example, you wouldn’t be able to invoke the &lt;code&gt;Ctrl + ]&lt;/code&gt; shortcut in the ЙЦУКЕН layout. The &lt;code&gt;event.key&lt;/code&gt; value is a non-Latin letter &lt;code&gt;ъ&lt;/code&gt;, but the &lt;code&gt;event.code&lt;/code&gt; is &lt;code&gt;BracketRight&lt;/code&gt; and not &lt;code&gt;Key*&lt;/code&gt;.&lt;/p&gt;
  &lt;figure id=&quot;wGC4&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/48/2d/482dd05d-848b-470f-a89f-f67a7443efe6.jpeg&quot; width=&quot;157&quot; /&gt;
    &lt;figcaption&gt;The key is non-Latin letter ъ, but the code doesn’t refer to a Latin letter.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;NshL&quot;&gt;This can be solved with a more robust mapping between codes and symbols of the QWERTY layout, but I would just avoid using punctuation symbols in shortcuts altogether.&lt;/p&gt;
  &lt;p id=&quot;QbCt&quot;&gt;There’s a good reason for this. Lesser-used punctuation symbols, like square and curly brackets, ampersands, backslashes, and so on, are omitted from many keyboard layouts to make room for larger alphabets. This means keyboard shortcuts like &lt;code&gt;Ctrl + ]&lt;/code&gt; can confuse your users because they don’t know offhand where to find symbols like &lt;code&gt;]&lt;/code&gt; on their keyboards.&lt;/p&gt;
  &lt;p id=&quot;fCZQ&quot;&gt;More common punctuation, like commas, can be found in most keyboard layouts, but they are not always in the same place as in QWERTY (for example, in ЙЦУКЕН, you should press &lt;code&gt;Shift + .&lt;/code&gt; to enter a comma). So shortcuts like &lt;code&gt;Ctrl + ,&lt;/code&gt; are ambiguous, because it’s not immediately clear whether a user should be looking for a comma in QWERTY or in their local layout.&lt;/p&gt;
  &lt;p id=&quot;RCJt&quot;&gt;Unless you’re building professional software with &lt;em&gt;lots&lt;/em&gt; of keyboard actions, it’s better to just stick to letters for your shortcuts.&lt;/p&gt;

</content></entry><entry><id>smalldogenergy:designing-suspenseful-hooks</id><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy/designing-suspenseful-hooks?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><title>Designing Suspenseful Hooks</title><published>2024-02-07T20:01:15.885Z</published><updated>2024-03-07T00:21:29.767Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/d8/3a/d83af6dd-e8ab-41ea-b381-eb4e34e0b08d.png"></media:thumbnail><category term="in-english" label="In English"></category><summary type="html">Your library hooks should not suspend. Just return a promise, and let the developer decide when to suspend.</summary><content type="html">
  &lt;p id=&quot;ZgpI&quot;&gt;&lt;a href=&quot;https://react.dev/reference/react/Suspense&quot; target=&quot;_blank&quot;&gt;React Suspense&lt;/a&gt; is not just about data fetching and loaders. But, it’s also &lt;em&gt;mostly&lt;/em&gt; about data fetching and loaders, and it’s a really good API for that. So it makes sense that data fetching libraries, like &lt;code&gt;@tanstack/react-query&lt;/code&gt; or &lt;code&gt;swr&lt;/code&gt;, provide “suspenseful” versions of their hooks.&lt;/p&gt;
  &lt;p id=&quot;5wMg&quot;&gt;Except that the way their suspenseful hooks are implemented (dare I say it) kinda goes against the idea of Suspense.&lt;/p&gt;
  &lt;p id=&quot;Vy4Y&quot;&gt;Consider the following component, which makes two requests with &lt;code&gt;react-query&lt;/code&gt; v5:&lt;/p&gt;
  &lt;pre id=&quot;fUoT&quot; data-lang=&quot;jsx&quot;&gt;// UserProfile.jsx
function UserProfile({ userId }) {
  const { data: user, isPending: isUserPending } = useQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId],
    queryFn: () =&amp;gt; fetchUser(userId),
  });

  const { data: friends, isPending: isFriendsPending } = useQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId, &amp;quot;friends&amp;quot;],
    queryFn: () =&amp;gt; fetchFriends(userId),
  });

  if (isUserPending || isFriendsPending) {
    return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h2&amp;gt;{user.name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Friends: {friends.length}&amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
  &lt;pre id=&quot;cryC&quot; data-lang=&quot;jsx&quot;&gt;// index.jsx
root.render(
  &amp;lt;&amp;gt;
    &amp;lt;h1&amp;gt;User Profile&amp;lt;/h1&amp;gt;
    &amp;lt;UserProfile userId=&amp;quot;some-id&amp;quot; /&amp;gt;
  &amp;lt;/&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;l3pM&quot;&gt;In a perfect world, there’s a single endpoint that returns all the data we need for our &lt;code&gt;UserProfile&lt;/code&gt; component, including both user data and the amount of friends the user has. But we can’t always dictate how an API works, so it’s fairly common for a single component to make multiple requests.&lt;/p&gt;
  &lt;p id=&quot;UUQ1&quot;&gt;Currently our &lt;code&gt;UserProfile&lt;/code&gt; component handles both fetching the data and the loading state. The cool thing about Suspense is that it allows us to write components as if the data always exists, and not have to deal with the loading state at the component level. Let’s try using a suspenseful version of the &lt;code&gt;useQuery&lt;/code&gt; hook, &lt;code&gt;useSuspenseQuery&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;aBh5&quot; data-lang=&quot;jsx&quot;&gt;// UserProfile.jsx
function UserProfile({ userId }) {
  const user = useSuspenseQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId],
    queryFn: () =&amp;gt; fetchUser(userId),
  });

  const friends = useSuspenseQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId, &amp;quot;friends&amp;quot;],
    queryFn: () =&amp;gt; fetchFriends(userId),
  });

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;{user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Friends: {friends.length}&amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
  &lt;pre id=&quot;BwsG&quot; data-lang=&quot;jsx&quot;&gt;// index.jsx
root.render(
  &amp;lt;&amp;gt;
    &amp;lt;h1&amp;gt;User Profile&amp;lt;/h1&amp;gt;
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;UserProfile userId=&amp;quot;some-id&amp;quot; /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  &amp;lt;/&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;H2Gq&quot;&gt;This seems great at the first glance, but there’s a problem. Previously, the &lt;code&gt;UserProfile&lt;/code&gt; component initiated two parallel requests:&lt;/p&gt;
  &lt;figure id=&quot;TR69&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/e8/43/e843f422-d9ad-49de-bfd0-82ed36363b9c.png&quot; width=&quot;705&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;F0m6&quot;&gt;Now those same two requests are serial. We’ve introduced a waterfall:&lt;/p&gt;
  &lt;figure id=&quot;sPo1&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8f/6e/8f6efc8a-9e24-4363-97b7-3346d3fe15a8.png&quot; width=&quot;913&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;N9OE&quot;&gt;And this makes sense. The &lt;code&gt;useSuspenseQuery&lt;/code&gt; hook will suspend rendering if the data is not ready yet. Suspending happens by &lt;a href=&quot;https://github.com/TanStack/query/blob/main/packages/react-query/src/useBaseQuery.ts#L102&quot; target=&quot;_blank&quot;&gt;throwing a promise&lt;/a&gt;, so once the first query suspends rendering, the second query doesn’t even have a chance to run until rendering is unsuspended.&lt;/p&gt;
  &lt;figure id=&quot;TeHF&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/bb/12/bb120765-314b-4fed-9b81-424d9be99d4b.png&quot; width=&quot;2234&quot; /&gt;
  &lt;/figure&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;doRR&quot;&gt;&lt;strong&gt;How does the component get suspended?&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;FW0G&quot;&gt;Although it’s not currently documented, this is basically how Suspense works: throwing a &lt;code&gt;Promise&lt;/code&gt; object from the component during render causes React to scrap the entire component subtree up to the nearest Suspense boundary (similar to how throwing an error from render is handled by the nearest error boundary).&lt;/p&gt;
    &lt;p id=&quot;GvTV&quot;&gt;React will then render a fallback and continue working on other subtrees. When the thrown promise resolves, React renders the suspended subtree again. In the case of &lt;code&gt;react-query&lt;/code&gt;, the fact that the promise is resolved means that the data is cached, so this time the hook doesn’t throw and instead returns the data from the cache.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;0lu6&quot;&gt;The solution &lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/request-waterfalls#:~:text=always%20using%20the%20hook%20useSuspenseQueries%20when%20you%20have%20multiple%20suspenseful%20queries&quot; target=&quot;_blank&quot;&gt;suggested&lt;/a&gt; by &lt;code&gt;react-query&lt;/code&gt; is to use the &lt;code&gt;useSuspenseQueries&lt;/code&gt; hook. Instead of using &lt;code&gt;useSuspenseQuery&lt;/code&gt; twice, you write:&lt;/p&gt;
  &lt;pre id=&quot;Xq18&quot; data-lang=&quot;jsx&quot;&gt;// UserProfile.jsx
const [user, friends] = useSuspenseQueries({
  queries: [
    {
      queryKey: [&amp;quot;user&amp;quot;, userId],
      queryFn: () =&amp;gt; fetchUser(userId),
    },
    {
      queryKey: [&amp;quot;user&amp;quot;, userId, &amp;quot;friends&amp;quot;],
      queryFn: () =&amp;gt; fetchFriends(userId),
    },
  ],
});&lt;/pre&gt;
  &lt;p id=&quot;XYXv&quot;&gt;This prevents the waterfall from happening, but in every other way it is a downgrade. It doesn’t look as nice, but more importantly, it’s less composable. We used to be able to extract our queries into reusable custom hooks:&lt;/p&gt;
  &lt;pre id=&quot;Xwu9&quot; data-lang=&quot;jsx&quot;&gt;// api.js
export function useUser(userId) {
  return useSuspenseQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId],
    queryFn: () =&amp;gt; fetchUser(userId),
  });
}

export function useUserFriends(userId) {
  return useSuspenseQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId, &amp;quot;friends&amp;quot;],
    queryFn: () =&amp;gt; fetchFriends(userId),
  });
}&lt;/pre&gt;
  &lt;p id=&quot;DJnH&quot;&gt;The developer importing these hooks didn’t have to worry about &lt;code&gt;react-query&lt;/code&gt; at all, and could compose them however they wanted. Now, our best option is to provide reusable query configs and to ask the developer to use &lt;code&gt;react-query&lt;/code&gt; hooks directly, which is not nearly as nice.&lt;/p&gt;
  &lt;p id=&quot;7rKK&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;∗ ∗ ∗&lt;br /&gt;&lt;/p&gt;
  &lt;p id=&quot;B2AU&quot;&gt;&lt;strong&gt;The root cause of our problems is that the &lt;code&gt;useSuspenseQuery&lt;/code&gt; hook is suspending too early.&lt;/strong&gt; We haven’t even had a chance to use the data in any meaningful way, but our component is already suspended!&lt;/p&gt;
  &lt;p id=&quot;u28b&quot;&gt;Now imagine that instead of throwing and suspending, the hook we use to fetch the data returns a promise. We then use a special helper function (let’s call it &lt;code&gt;use&lt;/code&gt;) that either returns the value of the promise &lt;em&gt;synchronously&lt;/em&gt;, or throws the promise and suspends rendering if the promise has yet to be fulfilled. (You could say that the &lt;code&gt;use&lt;/code&gt; function unpacks a value inside the promise, and throws the promise if it cannot be unpacked yet.)&lt;/p&gt;
  &lt;p id=&quot;yOEP&quot;&gt;Then our code might look something like this:&lt;/p&gt;
  &lt;pre id=&quot;RJkZ&quot; data-lang=&quot;jsx&quot;&gt;function use(promise) {
  /* We&amp;#x27;ll implement it later */
}

// UserProfile.jsx
function UserProfile({ userId }) {
  const user = useUser();
  const friends = useFriends();

  return (
    &amp;lt;&amp;gt;
      {/* unpack 👇 user before accessing */}
      &amp;lt;h1&amp;gt;{use(user).name}&amp;lt;/h1&amp;gt;
      {/* unpack freinds 👇 before accessing */}
      &amp;lt;p&amp;gt;Friends: {use(friends).length}&amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
  &lt;pre id=&quot;6T6w&quot; data-lang=&quot;jsx&quot;&gt;// index.jsx
root.render(
  &amp;lt;&amp;gt;
    &amp;lt;h1&amp;gt;User Profile&amp;lt;/h1&amp;gt;
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;UserProfile userId=&amp;quot;some-id&amp;quot; /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  &amp;lt;/&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;VWgb&quot;&gt;Let’s take a line-by-line look at how React renders this component.&lt;/p&gt;
  &lt;p id=&quot;YVQI&quot;&gt;First, two requests are made in parallel:&lt;/p&gt;
  &lt;pre id=&quot;qggs&quot; data-lang=&quot;jsx&quot;&gt;const user = useUser();
const friends = useFriends();&lt;/pre&gt;
  &lt;p id=&quot;zBLZ&quot;&gt;The hooks don’t suspend, and they don’t actually return the data: instead, they initiate requests, and return promises associated with those requests.&lt;/p&gt;
  &lt;p id=&quot;unz3&quot;&gt;Then, we try to unpack the first promise:&lt;/p&gt;
  &lt;pre id=&quot;OLbT&quot; data-lang=&quot;jsx&quot;&gt;&amp;lt;h1&amp;gt;{use(user).name}&amp;lt;/h1&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;KMoM&quot;&gt;If the user data isn’t already cached (which it probably isn’t), the call to &lt;code&gt;use(user)&lt;/code&gt; will throw a promise and suspend the component.&lt;/p&gt;
  &lt;p id=&quot;SWGV&quot;&gt;Once this promise is fulfilled, React will try to render &lt;code&gt;UserProfile&lt;/code&gt; again. This time, no new requests will be initiated: the user data is already cached by our library, so there’s no need for a new request; the friends data may also already be cached at this point, but if it is not, the request should still be deduplicated based on the query key.&lt;/p&gt;
  &lt;p id=&quot;wOQC&quot;&gt;During this second render, &lt;code&gt;use(user)&lt;/code&gt; doesn’t throw and instead returns the cached data. If the friends data is also ready, then &lt;code&gt;use(friends)&lt;/code&gt; won’t throw either, and we’re done. If not, &lt;code&gt;use(friends)&lt;/code&gt; will throw a promise — and we’ll go through the whole cycle again.&lt;/p&gt;
  &lt;figure id=&quot;EwJR&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/a1/a0/a1a0188e-3368-4c5e-b0c4-eedaf79faf7e.png&quot; width=&quot;2240&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;ejzT&quot;&gt;The hypothetical &lt;code&gt;use&lt;/code&gt; function is not that difficult to implement in practice:&lt;/p&gt;
  &lt;pre id=&quot;zR9W&quot; data-lang=&quot;jsx&quot;&gt;function use(promise) {
  if (promise.status === &amp;quot;fulfilled&amp;quot;) {
    return promise.value;
  }

  if (promise.status === &amp;quot;rejected&amp;quot;) {
    throw promise.reason;
  }

  if (promise.status === &amp;quot;pending&amp;quot;) {
    throw promise;
  }

  promise.status = &amp;quot;pending&amp;quot;;
  promise.then(
    (value) =&amp;gt; {
      promise.status = &amp;quot;fulfilled&amp;quot;;
      promise.value = value;
    },
    (reason) =&amp;gt; {
      promise.status = &amp;quot;rejected&amp;quot;;
      promise.reason = reason;
    }
  );

  throw promise;
}&lt;/pre&gt;
  &lt;p id=&quot;9AEF&quot;&gt;An important thing to note here is that we mutate the promise object to “smuggle” the value it was resolved with — or the error it was rejected with — onto the promise itself. That way, the next time &lt;code&gt;use&lt;/code&gt; is called with the same promise, we can return the value &lt;em&gt;synchronously&lt;/em&gt;. This is important because the implementation of &lt;code&gt;use&lt;/code&gt; can’t be async, otherwise we won’t be able to use it during rendering.&lt;/p&gt;
  &lt;p id=&quot;gcBR&quot;&gt;Another, perhaps cleaner way to achieve the same thing is to have a global WeakMap that maps promises to their values:&lt;/p&gt;
  &lt;pre id=&quot;TaLN&quot; data-lang=&quot;jsx&quot;&gt;const promiseResults = new WeakMap();

function use(promise) {
  const result = promiseResults.get(promise);

  if (result) {
    if (result.status === &amp;quot;fulfilled&amp;quot;) {
      return result.value;
    }

    if (result.status === &amp;quot;rejected&amp;quot;) {
      throw result.reason;
    }

    throw promise;
  }

  promiseResults.set(promise, { status: &amp;quot;pending&amp;quot; });
  promise.then(
    (value) =&amp;gt; {
      promiseResults.set(promise, { status: &amp;quot;fulfilled&amp;quot;, value });
    },
    (reason) =&amp;gt; {
      promiseResults.set(promise, { status: &amp;quot;rejected&amp;quot;, reason });
    }
  );

  throw promise;
}&lt;/pre&gt;
  &lt;p id=&quot;oifs&quot;&gt;(Without smuggling values onto the promise object, this implementation feels less hacky to me.)&lt;/p&gt;
  &lt;p id=&quot;E7FV&quot;&gt;Whichever implementation we choose, you may have noticed a potential pitfall. In order for the &lt;code&gt;use&lt;/code&gt; function to work properly, we must pass &lt;em&gt;the same instance&lt;/em&gt; of the promise for each fetched value. This means that the data fetching library must be very careful not to accidentally create new promises on subsequent renders.&lt;/p&gt;
  &lt;p id=&quot;TBoR&quot;&gt;Clearly, this implementation will not work:&lt;/p&gt;
  &lt;pre id=&quot;Rk7y&quot; data-lang=&quot;jsx&quot;&gt;// This will not work!
function useQuery({ queryKey, queryFn }) {
  return queryFn();
}&lt;/pre&gt;
  &lt;p id=&quot;HkEA&quot;&gt;It creates a new promise on each render, so the &lt;code&gt;use&lt;/code&gt; function will always throw. Instead, we need to cache our promises:&lt;/p&gt;
  &lt;pre id=&quot;UTzT&quot; data-lang=&quot;jsx&quot;&gt;const cache = new Map();

function useQuery({ queryKey, queryFn }) {
  const key = queryKey.join(&amp;quot;.&amp;quot;);
  const entry = cache.get(key);

  if (!entry) {
    entry = queryFn();
    cache.set(key, entry);
  }

  return entry;
}&lt;/pre&gt;
  &lt;p id=&quot;BqBw&quot;&gt;We must also be careful to avoid calling an async function on every render:&lt;/p&gt;
  &lt;pre id=&quot;qFTy&quot; data-lang=&quot;jsx&quot;&gt;async function fetchOrGetCached({ queryKey, queryFn }) {
  /* ... */
}

function useQuery(options) {
  return fetchOrGetCached(options);
}&lt;/pre&gt;
  &lt;p id=&quot;GHl5&quot;&gt;The caveat here is that &lt;code&gt;fetchOrGetCached&lt;/code&gt; creates a new promise with each call, even if it is resolved immediately. So, again, the &lt;code&gt;use&lt;/code&gt; function will always get a brand new promise and throw.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;Ron2&quot;&gt;&lt;strong&gt;Why even make &lt;code&gt;useQuery&lt;/code&gt; a hook at this point?&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;bnW7&quot;&gt;Good question! Our implementation of &lt;code&gt;useQuery&lt;/code&gt; doesn’t call any React hooks, so it doesn’t need to be a hook itself. In real life, however, it will probably create a subscription (e.g. with &lt;code&gt;useSyncExternalStore&lt;/code&gt;) to update the component when the stale data is refetched, or when the cache is invalidated.&lt;/p&gt;
    &lt;p id=&quot;SCFm&quot;&gt;Still, it’s a good idea to also provide a non-hook version of &lt;code&gt;useQuery&lt;/code&gt;, so that requests can be made not only by React components, but also by regular JavaScript code.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;bXyO&quot;&gt;With a little care around our promises, we get the desired experience: requests run in parallel, we only suspend when the data is actually read, and developers can compose as many data-fetching hooks in their components as they like.&lt;/p&gt;
  &lt;p id=&quot;K0xv&quot;&gt;It even unlocks new patterns, such as passing promises as props and suspending further down the tree:&lt;/p&gt;
  &lt;pre id=&quot;8IoW&quot; data-lang=&quot;jsx&quot;&gt;// UserProfile.jsx
function UserProfile({ userId }) {
  const user = useQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId],
    queryFn: () =&amp;gt; fetchUser(userId),
  });

  const friends = useQuery({
    queryKey: [&amp;quot;user&amp;quot;, userId, &amp;quot;friends&amp;quot;],
    queryFn: () =&amp;gt; fetchFriends(userId),
  });

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;{use(user).name}&amp;lt;/h1&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading friends...&amp;lt;/p&amp;gt;}&amp;gt;
        &amp;lt;FriendsList friends={friends} /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/pre&gt;
  &lt;pre id=&quot;nnf5&quot; data-lang=&quot;jsx&quot;&gt;// FriendsList.jsx
function FriendsList({ friends }) {
  return use(friends).map((friend) =&amp;gt; {
    /* ... */
  });
}&lt;/pre&gt;
  &lt;pre id=&quot;321K&quot; data-lang=&quot;jsx&quot;&gt;// index.jsx
root.render(
  &amp;lt;&amp;gt;
    &amp;lt;h1&amp;gt;User Profile&amp;lt;/h1&amp;gt;
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading profile...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;UserProfile userId=&amp;quot;some-id&amp;quot; /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  &amp;lt;/&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;40n2&quot;&gt;Because &lt;code&gt;UserProfile&lt;/code&gt; doesn’t try to unpack the friends data, it will never suspend on it. Instead, it’s the &lt;code&gt;FriendsList&lt;/code&gt; component that will suspend if the friends data is not ready. If the friends data takes longer to load than the user data, we’ll see the user profile partially loaded with the friends list suspended and the “Loading friends...” loader visible.&lt;/p&gt;
  &lt;figure id=&quot;qGZm&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/79/5d/795d0df6-c615-486d-8f99-334c5b170640.png&quot; width=&quot;2976&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;4bJW&quot;&gt;This pattern comes in handy when we want to start fetching data as soon as possible to avoid waterfalls, and to gradually reveal parts of the UI as the data trickles in.&lt;/p&gt;
  &lt;p id=&quot;fPkF&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;∗ ∗ ∗&lt;br /&gt;&lt;/p&gt;
  &lt;p id=&quot;kivG&quot;&gt;So! If there’s a single takeaway from this lengthy article, it’s this one:&lt;/p&gt;
  &lt;p id=&quot;RjDP&quot;&gt;&lt;strong&gt;Your library hooks should not suspend. Just return a promise, and let the developer decide when to suspend.&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;u0w0&quot;&gt;And the best news: React 19 &lt;a href=&quot;https://react.dev/reference/react/use&quot; target=&quot;_blank&quot;&gt;will include&lt;/a&gt; an out-of-the-box implementation of the &lt;code&gt;use&lt;/code&gt; hook! Until then, you can ship one of the implementations from this article along with your library, and later provide a migration guide for React 19 users. Or, why not publish a polyfill for the &lt;code&gt;use&lt;/code&gt; hook right now? It’s a useful pattern that can benefit React 18 users as well.&lt;/p&gt;
  &lt;p id=&quot;kGEI&quot;&gt;As an aside, it’s no wonder that developers are confused about how best to use Suspense in their libraries, since there’s no real guidance from the React team. It’s been two years since Suspense was released in React 18, and there’s still no documentation for library authors.&lt;/p&gt;
  &lt;p id=&quot;vvb7&quot;&gt;Hopefully, with React 19, we’ll get proper documentation, leading to wider (and more idiomatic) adoption of Suspense in both libraries and applications.&lt;/p&gt;

</content></entry><entry><id>smalldogenergy:right-idea</id><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy/right-idea?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><title>The Right Idea</title><published>2023-10-28T15:02:26.662Z</published><updated>2024-02-08T15:18:45.946Z</updated><category term="in-english" label="In English"></category><summary type="html">Every now and then a project comes along that feels like The Right Idea.</summary><content type="html">
  &lt;p id=&quot;4oLs&quot;&gt;Not a week goes by on Ex-Twitter without hype for a new frontend library. Yet every now and then a project comes along that feels like The Right Idea.&lt;/p&gt;
  &lt;p id=&quot;2COD&quot;&gt;The Right Idea is more of a vibe than anything objective. The Right Idea project is not always the best tool for the job (although some of them are). But I believe in the approach these projects have taken, and I think others could benefit from taking inspiration in their direction.&lt;/p&gt;
  &lt;h2 id=&quot;Cbb8&quot;&gt;&lt;a href=&quot;https://react.dev/&quot; target=&quot;_blank&quot;&gt;React&lt;/a&gt;&lt;/h2&gt;
  &lt;p id=&quot;fIik&quot;&gt;React’s progress is slow, sometimes painfully so. And yet, each new feature introduced fits so neatly into place that it almost feels like an academic project, a “general &lt;a href=&quot;https://overreacted.io/react-as-a-ui-runtime/&quot; target=&quot;_blank&quot;&gt;UI runtime&lt;/a&gt;”.&lt;/p&gt;
  &lt;p id=&quot;r3mi&quot;&gt;JSX, Hooks, Suspense, Server Components are all good, clear, and inspired ideas. They also have a tendency to reveal a deeper, more general idea behind them over time.&lt;/p&gt;
  &lt;p id=&quot;GxTh&quot;&gt;Hooks are not just a way to achieve feature parity between functional and class components. They are a novel approach to sharing logic between components. They are also &lt;a href=&quot;https://overreacted.io/algebraic-effects-for-the-rest-of-us/&quot; target=&quot;_blank&quot;&gt;algebraic effects&lt;/a&gt; that abstract away non-pure parts of a render function, allowing for a much simpler mental model of rendering.&lt;/p&gt;
  &lt;p id=&quot;ZiRC&quot;&gt;Suspense, it turns out, is not just about loaders. It’s a way to fork your component tree and render new state in the background. It’s also a way to postpone rendering part of your page on the server and stream it in later.&lt;/p&gt;
  &lt;p id=&quot;74M1&quot;&gt;I can’t wait to see what kind of impact Server Components are going to have in the future!&lt;/p&gt;
  &lt;h2 id=&quot;miG4&quot;&gt;&lt;a href=&quot;https://react-spectrum.adobe.com/&quot; target=&quot;_blank&quot;&gt;React Spectrum&lt;/a&gt;&lt;/h2&gt;
  &lt;p id=&quot;5SMO&quot;&gt;Spectrum is a component library built in layers: there’s presentation and there’s behavior.&lt;/p&gt;
  &lt;p id=&quot;TFo4&quot;&gt;&lt;a href=&quot;https://react-spectrum.adobe.com/react-aria/&quot; target=&quot;_blank&quot;&gt;React Aria&lt;/a&gt; implements behavior for different types of components and user interactions. It provides a set of hooks that make minimal assumptions about what the final component will look like. It just exports hooks and doesn’t provide any actual components.&lt;/p&gt;
  &lt;p id=&quot;nHAW&quot;&gt;React Aria is pretty low-level, so there are also &lt;a href=&quot;https://react-spectrum.adobe.com/react-aria/react-aria-components.html&quot; target=&quot;_blank&quot;&gt;React Aria Components&lt;/a&gt;, which provide an unstyled implementation of components based on React Aria hooks.&lt;/p&gt;
  &lt;p id=&quot;4vdK&quot;&gt;This feels like The Right Idea of extensibility and reusability in a component library.&lt;/p&gt;
  &lt;h2 id=&quot;hJVC&quot;&gt;&lt;a href=&quot;https://ui.shadcn.com/&quot; target=&quot;_blank&quot;&gt;shadcn/ui&lt;/a&gt;&lt;/h2&gt;
  &lt;p id=&quot;CGPQ&quot;&gt;Has the right idea about how component libraries should be distributed. Instead of importing a component and then overriding its style with themes or whatever, just copy and paste the whole component and edit the Tailwind classes directly.&lt;/p&gt;
  &lt;p id=&quot;8BtY&quot;&gt;Too bad it’s based on Radix UI and not React Aria Components :)&lt;/p&gt;
  &lt;h2 id=&quot;1uf1&quot;&gt;&lt;a href=&quot;https://tailwindcss.com/&quot; target=&quot;_blank&quot;&gt;Tailwind&lt;/a&gt;&lt;/h2&gt;
  &lt;p id=&quot;FBE7&quot;&gt;For a long time I thought &lt;a href=&quot;https://github.com/css-modules/css-modules&quot; target=&quot;_blank&quot;&gt;CSS Modules&lt;/a&gt; were the way to go. To me, Tailwind looked like unreadable CSS with extra steps.&lt;/p&gt;
  &lt;p id=&quot;ExIV&quot;&gt;Now I think that the whole idea of separating styles from semantic markup, which started with CSS 1.0 in 1996, was &lt;a href=&quot;https://siderea.dreamwidth.org/1819759.html&quot; target=&quot;_blank&quot;&gt;wrong&lt;/a&gt; to begin with. There is no “semantic” markup. It’s all just content, and styles are content too.&lt;/p&gt;
  &lt;p id=&quot;ARg9&quot;&gt;Tailwind fixes this decades-old mistake by putting styles and markup back together.&lt;/p&gt;

</content></entry><entry><id>smalldogenergy:locators-en</id><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy/locators-en?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><title>Stop Using String Field Names in Your Forms (and Use Locators Instead)</title><published>2023-10-25T20:02:43.782Z</published><updated>2024-02-08T15:18:30.845Z</updated><category term="in-english" label="In English"></category><summary type="html">Introducing an interesting type of objects called locators. Using locators instead of strings for field names makes implementing type-safe forms just so much easier.</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;PNns&quot;&gt;&lt;em&gt;You can also read this article &lt;a href=&quot;https://teletype.in/@smalldogenergy/locators&quot; target=&quot;_blank&quot;&gt;in Russian&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;Uc2y&quot;&gt;As Leo Tolstoy said, all React form libraries are alike; each one makes me unhappy in its own way. Or was it something to do with families?&lt;/p&gt;
  &lt;p id=&quot;kIb1&quot;&gt;Anyway, here’s a typical form made with &lt;a href=&quot;https://formik.org/&quot; target=&quot;_blank&quot;&gt;Formik&lt;/a&gt;:&lt;/p&gt;
  &lt;pre id=&quot;uU6x&quot; data-lang=&quot;tsx&quot;&gt;&amp;lt;Formik
  initialValues={{ email: &amp;quot;&amp;quot; }}
  onSubmit={(values) =&amp;gt; {
    /* ... */
  }}
&amp;gt;
  &amp;lt;Form&amp;gt;
    &amp;lt;Field name=&amp;quot;email&amp;quot; as={EmailInput} /&amp;gt;
  &amp;lt;/Form&amp;gt;
&amp;lt;/Formik&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;so5L&quot;&gt;It’s trivially easy to misspell a field name. In this case, it would be reasonable to expect a type error during compilation, or at least a runtime error:&lt;/p&gt;
  &lt;pre id=&quot;xcOs&quot; data-lang=&quot;tsx&quot;&gt;      // typo! 👇
&amp;lt;Field name=&amp;quot;notmail&amp;quot; as={EmailInput} /&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;4w8J&quot;&gt;Unfortunately, Formik does nothing to help us find the typo. What we get instead are unexpected form values in the &lt;code&gt;onSubmit&lt;/code&gt; callback.&lt;/p&gt;
  &lt;p id=&quot;itS9&quot;&gt;Another source of unexpected behavior is incompatible components. Let us say we’re using a custom component for our email input that looks something like this:&lt;/p&gt;
  &lt;pre id=&quot;HIj7&quot; data-lang=&quot;tsx&quot;&gt;function EmailInput(props: {
  value: string;
  onChange: (value: string) =&amp;gt; void;
         // that&amp;#x27;s 👆 not compatible with Formik!
}) {
  /* ... */
}&lt;/pre&gt;
  &lt;p id=&quot;pthj&quot;&gt;It’s difficult to spot, but the signature for the &lt;code&gt;onChange&lt;/code&gt; callback of this component is incompatible with Formik. Again, we won’t get compilation or runtime errors. Instead, the form just won’t work.&lt;/p&gt;
  &lt;p id=&quot;Tihb&quot;&gt;The reason that Formik cannot catch these types of errors is that the &lt;code&gt;Field&lt;/code&gt; component doesn’t know anything about our form schema. Without the information about the form schema, the &lt;code&gt;Field&lt;/code&gt; component cannot check if a field name is valid or if a component has compatible prop types for that field.&lt;/p&gt;
  &lt;p id=&quot;bhUt&quot;&gt;Let’s take a look at the more modern &lt;a href=&quot;https://react-hook-form.com/&quot; target=&quot;_blank&quot;&gt;React Hook Form&lt;/a&gt;:&lt;/p&gt;
  &lt;pre id=&quot;Yi7H&quot; data-lang=&quot;tsx&quot;&gt;const { handleSubmit, control } = useForm({
  defaultValues: { email: &amp;quot;&amp;quot; },
});

return (
  &amp;lt;form
    onSubmit={handleSubmit((values) =&amp;gt; {
      /* ... */
    })}
  &amp;gt;
    &amp;lt;Controller
      name=&amp;quot;email&amp;quot;
      control={control}
      render={({ field }) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
    /&amp;gt;
  &amp;lt;/form&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;1jvn&quot;&gt;Right out of the box, the situation is much better. A typo in a field name is immediately caught by the type system. That’s thanks to a neat &lt;code&gt;FieldPath&lt;/code&gt; type defined within the library:&lt;/p&gt;
  &lt;pre id=&quot;T9c7&quot; data-lang=&quot;typescript&quot;&gt;FieldPath&amp;lt;{ users: { email: string }[] }&amp;gt;
// &amp;quot;users&amp;quot; | &amp;#x60;users.${number}&amp;#x60; | &amp;#x60;users.${number}.email&amp;#x60;&lt;/pre&gt;
  &lt;p id=&quot;L2NH&quot;&gt;React Hook Form also checks for some potential component compatibility issues:&lt;/p&gt;
  &lt;pre id=&quot;qQ1I&quot; data-lang=&quot;tsx&quot;&gt;function EmailInput(props: {
  value: number;
      // 👆 that&amp;#x27;s the wrong type for email!
  onChange: (value: string) =&amp;gt; void;
}) {
  /* ... */
}

&amp;lt;Controller
  name=&amp;quot;email&amp;quot;
  control={control}
  render={({ field }) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
/&amp;gt;;
// 🔴 Error:
// Types of property &amp;#x27;value&amp;#x27; are incompatible.
// Type &amp;#x27;string&amp;#x27; is not assignable to type &amp;#x27;number&amp;#x27;.&lt;/pre&gt;
  &lt;p id=&quot;eCAp&quot;&gt;However, it’s not without its own drawbacks. Yes, it requires a component’s &lt;code&gt;value&lt;/code&gt; prop type to be compatible with the field. But at the same time React Hook Form doesn’t check for the &lt;code&gt;onChange&lt;/code&gt; callback compatibility. Still, this can be fixed in a future release.&lt;/p&gt;
  &lt;p id=&quot;BvRm&quot;&gt;More importantly, all these type safety features are based on the form schema information inferred from the type of the &lt;code&gt;control&lt;/code&gt; object. This means no type safety is guaranteed when the &lt;code&gt;control&lt;/code&gt; object is passed implicitly via the &lt;code&gt;FormProvider&lt;/code&gt; context:&lt;/p&gt;
  &lt;pre id=&quot;RiJR&quot; data-lang=&quot;tsx&quot;&gt;const methods = useForm({
  defaultValues: { email: &amp;quot;&amp;quot; },
});

return (
  &amp;lt;FormProvider methods={methods}&amp;gt;
    &amp;lt;form&amp;gt;
      {/* that&amp;#x27;s a typo, but there won&amp;#x27;t be any errors! */}
      &amp;lt;Controller
        name=&amp;quot;notmail&amp;quot;
        render={({ field }) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
      /&amp;gt;
    &amp;lt;/form&amp;gt;
  &amp;lt;/FormProvider&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;mGqP&quot;&gt;And finally, even if the &lt;code&gt;control&lt;/code&gt; object is passed explicitly, sometimes these type safety features just aren’t very handy.&lt;/p&gt;
  &lt;p id=&quot;Rpkf&quot;&gt;Let’s say we want to implement a reusable component for editing an address within a form. An address consists of several fields, and since we want our component to be reusable, we want to pass a prefix for those fields as a prop:&lt;/p&gt;
  &lt;pre id=&quot;4rrS&quot; data-lang=&quot;tsx&quot;&gt;function AddressFieldset&amp;lt;T extends FieldValues&amp;gt;(props: {
  prefix: FieldPath&amp;lt;T&amp;gt;;
  control: Control&amp;lt;T&amp;gt;;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Controller
        control={props.control}
        name={&amp;#x60;${props.prefix}.city&amp;#x60;}
        render={() =&amp;gt; {
          /* ... */
        }}
      /&amp;gt;
      {/* ... */}
    &amp;lt;/&amp;gt;
  );
}
// 🔴 Error:
// Type &amp;#x27;&amp;#x60;${Path&amp;lt;T&amp;gt;}.city&amp;#x60;&amp;#x27; is not assignable to type &amp;#x27;Path&amp;lt;T&amp;gt;&amp;#x27;.&lt;/pre&gt;
  &lt;p id=&quot;FwMC&quot;&gt;Unsurprisingly, the type system doesn’t allow us to append &lt;code&gt;&amp;quot;.city&amp;quot;&lt;/code&gt; to a random field name since we cannot even be sure that the &lt;code&gt;prefix&lt;/code&gt; points to an address. In this case, our only option is to opt out of strict typing and just use &lt;code&gt;string&lt;/code&gt; instead of &lt;code&gt;FieldPath&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;EPdU&quot; data-lang=&quot;typescript&quot;&gt;function AddressFieldset(props: { prefix: string; control: Control }) {
  /* ... */
}&lt;/pre&gt;
  &lt;p id=&quot;HABs&quot;&gt;And that’s just unsportsmanlike!&lt;/p&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;EPwi&quot;&gt;Next, I’ll describe an interesting type of objects I’ll call &lt;strong&gt;“locators”&lt;/strong&gt;. Using locators instead of strings for field names makes implementing type-safe forms just so much easier.&lt;/p&gt;
  &lt;h2 id=&quot;pK96&quot;&gt;Introducing Locators&lt;/h2&gt;
  &lt;p id=&quot;RKHF&quot;&gt;I think it’s best to start with an example. Take a look at the form schema:&lt;/p&gt;
  &lt;pre id=&quot;wtD8&quot; data-lang=&quot;typescript&quot;&gt;type FormValues = {
  email: string;
  address: {
    city: string;
    street: string;
  };
};&lt;/pre&gt;
  &lt;p id=&quot;Es1m&quot;&gt;This is a locator for this form:&lt;/p&gt;
  &lt;pre id=&quot;KuH0&quot; data-lang=&quot;typescript&quot;&gt;const pathTag = Symbol(&amp;quot;path&amp;quot;);

const locator = {
  [pathTag]: &amp;quot;&amp;quot;,
  email: {
    [pathTag]: &amp;quot;email&amp;quot;,
  },
  address: {
    [pathTag]: &amp;quot;address&amp;quot;,
    city: { [pathTag]: &amp;quot;address.city&amp;quot; },
    street: { [pathTag]: &amp;quot;address.street&amp;quot; },
  },
};&lt;/pre&gt;
  &lt;p id=&quot;j2Qb&quot;&gt;You’ll notice two things about this object:&lt;/p&gt;
  &lt;ol id=&quot;kNYX&quot;&gt;
    &lt;li id=&quot;o7zo&quot;&gt;it has the same shape as the form values object, and&lt;/li&gt;
    &lt;li id=&quot;NltX&quot;&gt;instead of field values, it stores field paths.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;4NME&quot;&gt;You can say that a locator is an object that stores a path for each of its properties: &lt;code&gt;locator.email[pathTag]&lt;/code&gt; equals &lt;code&gt;&amp;quot;email&amp;quot;&lt;/code&gt;, and &lt;code&gt;locator.address.city[pathTag]&lt;/code&gt; equals &lt;code&gt;&amp;quot;address.city&amp;quot;&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;Bio2&quot;&gt;To add a type annotation to this locator, we could simply write something like this:&lt;/p&gt;
  &lt;pre id=&quot;glr0&quot; data-lang=&quot;typescript&quot;&gt;const pathTag = Symbol(&amp;quot;path&amp;quot;);

interface Locator {
  [pathTag]: string;
  email: {
    [pathTag]: string;
  };
  address: {
    [pathTag]: string;
    city: {
      [pathTag]: string;
    };
    street: {
      [pathTag]: string;
    };
  };
}&lt;/pre&gt;
  &lt;p id=&quot;eyBl&quot;&gt;But let’s go a step further. Along with a path for each form field stored in the locator’s value, we’ll actually have a type for each form field stored in the locator’s type:&lt;/p&gt;
  &lt;pre id=&quot;ZBkP&quot; data-lang=&quot;typescript&quot;&gt;const typeTag = Symbol(&amp;quot;type&amp;quot;);
const pathTag = Symbol(&amp;quot;path&amp;quot;);

interface Locator {
  [typeTag]?: FormValues;
  [pathTag]: string;
  email: {
    [typeTag]?: FormValues[&amp;quot;email&amp;quot;]; // 👈
    [pathTag]: string;
  };
  address: {
    [typeTag]?: FormValues[&amp;quot;address&amp;quot;]; // 👈
    [pathTag]: string;
    city: {
      [typeTag]?: FormValues[&amp;quot;address&amp;quot;][&amp;quot;city&amp;quot;]; // 👈
      [pathTag]: string;
    };
    street: {
      [typeTag]?: FormValues[&amp;quot;address&amp;quot;][&amp;quot;street&amp;quot;]; // 👈
      [pathTag]: string;
    };
  };
}&lt;/pre&gt;
  &lt;p id=&quot;5n2h&quot;&gt;The neat thing is, we don’t have to do it manually. We can construct locator type for any form schema with a recursive generic type:&lt;/p&gt;
  &lt;pre id=&quot;Haun&quot; data-lang=&quot;typescript&quot;&gt;const typeTag = Symbol(&amp;quot;type&amp;quot;);
const pathTag = Symbol(&amp;quot;path&amp;quot;);

type Locator&amp;lt;T&amp;gt; = {
  [typeTag]?: T;
  [pathTag]: string;
} &amp;amp; (T extends Record&amp;lt;string, unknown&amp;gt;
  ? { [K in keyof T]: Locator&amp;lt;T[K]&amp;gt; }
  : T extends (infer R)[]
  ? Locator&amp;lt;R&amp;gt;[]
  : {});&lt;/pre&gt;
  &lt;p id=&quot;xcUC&quot;&gt;But can we actually implement an object that matches this type? Sure we can! A locator is basically a string builder that appends a property name to a string each time that property is accessed. We can implement it using a proxy:&lt;/p&gt;
  &lt;pre id=&quot;6Wot&quot; data-lang=&quot;typescript&quot;&gt;function getLocator&amp;lt;T&amp;gt;(prefix: string = &amp;quot;&amp;quot;): Locator&amp;lt;T&amp;gt; {
  return new Proxy(
    {},
    {
      get(target, key) {
        if (key === pathTag) {
          return prefix;
        }
        
        if (typeof key === &amp;quot;symbol&amp;quot;) {
          throw new Error(&amp;quot;Symbol path segments are not allowed in locator&amp;quot;);
        }
        
        const nextPrefix = prefix ? &amp;#x60;${prefix}.${key}&amp;#x60; : key;
        return getLocator(nextPrefix);
      },
    }
  ) as Locator&amp;lt;T&amp;gt;;
}&lt;/pre&gt;
  &lt;section style=&quot;background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;BnBQ&quot;&gt;This is a reference implementation. There’s some room for optimazation, e.g. we can reuse the same &lt;code&gt;get&lt;/code&gt; handler for all proxies instead of creating a new closure each time.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;OJG5&quot;&gt;So, how do locators help us implement type-safe forms? Here’s what an API for our hypothetical locator-based form library might look like:&lt;/p&gt;
  &lt;pre id=&quot;OGJs&quot; data-lang=&quot;typescript&quot;&gt;function useForm&amp;lt;T&amp;gt;(options: { initialValues: T }): { n: Locator&amp;lt;T&amp;gt; } {
  return {
    n: getLocator&amp;lt;T&amp;gt;(), // 👈1️⃣
    /* ... */
  };
}

function Field&amp;lt;V&amp;gt;(props: {
  name: Locator&amp;lt;V&amp;gt;; // 👈2️⃣
  render: (field: {
    name: string;
    value: V; // 👈3️⃣
    onChange: (value: V) =&amp;gt; void;
  }) =&amp;gt; ReactElement;
}) {
  return props.render({
    name: props.name[pathTag],
    /* ... */
  });
}&lt;/pre&gt;
  &lt;p id=&quot;uNvc&quot;&gt;There are three things to note here:&lt;/p&gt;
  &lt;ol id=&quot;bE7N&quot;&gt;
    &lt;li id=&quot;XpSU&quot;&gt;The &lt;code&gt;useForm&lt;/code&gt; hook returns the “root” locator for the form schema, called &lt;code&gt;n&lt;/code&gt; for brevity.&lt;/li&gt;
    &lt;li id=&quot;nna8&quot;&gt;Locator is used instead of a string for the &lt;code&gt;name&lt;/code&gt; prop of the &lt;code&gt;Field&lt;/code&gt; component.&lt;/li&gt;
    &lt;li id=&quot;M8v6&quot;&gt;The &lt;code&gt;Field&lt;/code&gt; component can infer the value type of the field from the locator and type arguments for its render prop accordingly.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;0UkX&quot;&gt;The user of our library gets the root locator from the &lt;code&gt;useForm&lt;/code&gt; hook and must use it to construct field names:&lt;/p&gt;
  &lt;pre id=&quot;n3Wy&quot; data-lang=&quot;tsx&quot;&gt;const { n } = useForm({
  initialValues: {
    users: [
      {
        email: &amp;quot;&amp;quot;,
        address: {
          city: &amp;quot;&amp;quot;,
          street: &amp;quot;&amp;quot;,
        },
      },
    ],
  },
});

&amp;lt;Field
  name={n.users[0].email}
  render={(field) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
/&amp;gt;;&lt;/pre&gt;
  &lt;p id=&quot;Q5eM&quot;&gt;Since the locator’s type has the same shape as the form values, there’s no way to build a locator that points to a non-existent field. And the types for our field’s render prop ensure that whatever component is used for the field, it is compatible with the field’s value type.&lt;/p&gt;
  &lt;p id=&quot;x6Sz&quot;&gt;It’s also very easy to create a reusable set of fields:&lt;/p&gt;
  &lt;pre id=&quot;1hVU&quot; data-lang=&quot;tsx&quot;&gt;function AddressFieldset(props: {
  prefix: Locator&amp;lt;{ city: string; street: string }&amp;gt;;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Field
        name={props.prefix.city}
        render={() =&amp;gt; {
          /* ... */
        }}
      /&amp;gt;
      {/* ... */}
    &amp;lt;/&amp;gt;
  );
}

&amp;lt;AddressFieldset prefix={n.users[0].address} /&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;zGsm&quot;&gt;This component only accepts a locator pointing to an address in its &lt;code&gt;prefix&lt;/code&gt; prop. We can be sure that the value pointed to by this locator is indeed an address, and has all the properties an address should have.&lt;/p&gt;
  &lt;p id=&quot;XTvN&quot;&gt;Even better, locator types are covariant, so instead of passing an address, we can pass a locator that points to any value which is assignable to an address!&lt;/p&gt;
  &lt;p id=&quot;sl9F&quot;&gt;And as a cherry on top, you can totally rename a property on the &lt;code&gt;FormValues&lt;/code&gt; type, and your IDE will rename that property in every locator where it’s used. That’s super useful for refactoring!&lt;/p&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;oLrL&quot;&gt;There’s just one more thing: it’s still possible to accidentally pass a locator from one form to a field on another form:&lt;/p&gt;
  &lt;pre id=&quot;Jr4c&quot; data-lang=&quot;tsx&quot;&gt;const { form, n } = useForm({
  initialValues: { email: &amp;#x27;&amp;#x27; }
});

const { form: anotherForm, n: anotherN } = useForm({
  initialValues: { notemail: &amp;#x27;&amp;#x27; }
});

&amp;lt;FormProvider form={anotherForm}&amp;gt;
  {/* that&amp;#x27;s bad 👇 there&amp;#x27;s no &amp;quot;email&amp;quot; in &amp;quot;anotherForm&amp;quot; */}
  &amp;lt;Field name={n.email} render={() =&amp;gt; {
    /* ... */
  }} /&amp;gt;
&amp;lt;/FormProvider&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;BFMf&quot;&gt;We can’t catch that at build time. But at least we can handle this error at runtime by &lt;strong&gt;“branding”&lt;/strong&gt; our locators.&lt;/p&gt;
  &lt;p id=&quot;qV0G&quot;&gt;A brand is just a unique value that we’ll add to a form’s root locator. Each locator constructed from that root locator should have the same brand. And in the &lt;code&gt;Field&lt;/code&gt; component, we’ll check that the brand of the locator matches the brand of the form:&lt;/p&gt;
  &lt;pre id=&quot;lCZL&quot; data-lang=&quot;typescript&quot;&gt;function useForm&amp;lt;T&amp;gt;(options: { initialValues: T }) {
  const [brand] = useState(() =&amp;gt; Symbol(&amp;#x27;formBrand&amp;#x27;));

  return {
    // 1️⃣ the root locator &amp;quot;n&amp;quot; and all other locators constructed
    // from it will be branded by the same unique value
    n: getLocator&amp;lt;T&amp;gt;(brand),

    // 2️⃣ the same unique value is accessible via context as well
    form: { brand, /* ... */ },
  };
}

function Field&amp;lt;V&amp;gt;(props: {
  name: Locator&amp;lt;V&amp;gt;;
  /* ... */
}) {
  const { brand } = useContext(FormContext);

  // 3️⃣ matching the form brand with the locator brand
  if (brand !== name[brandTag]) {
    throw new Error(&amp;#x27;Foreign locator used to address form field&amp;#x27;);
  }

  /* ... */
}&lt;/pre&gt;
  &lt;p id=&quot;u9tw&quot;&gt;It’s a shame we can’t express the same guarantee in the type system. But unfortunately, React contexts are hard to type properly.&lt;/p&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;MsyO&quot;&gt;I’ve worked on a project with dozens of huge web forms, some with more than a hundred fields. (As you’ve probably guessed it, this job was in the B2B division of a large bank.) Ensuring a smooth user experience was key, but we couldn’t have done it without great developer experience. Taking the approach described in this article was a huge help. It gave us all the tools we needed to catch bugs early and ship with confidence.&lt;/p&gt;
  &lt;p id=&quot;8ukc&quot;&gt;I hope it helps you too!&lt;/p&gt;

</content></entry><entry><id>smalldogenergy:locators</id><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy/locators?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><title>Не используйте строковые идентификаторы полей в формах (используйте локаторы)</title><published>2023-03-06T02:06:14.588Z</published><updated>2023-03-07T10:17:43.966Z</updated><summary type="html">Все реактовые библиотеки для работы с формами похожи друг на друга (и каждая несчастлива по-своему).</summary><content type="html">
  &lt;p id=&quot;K3d9&quot;&gt;Все реактовые библиотеки для работы с формами похожи друг на друга (и каждая несчастлива по-своему).&lt;/p&gt;
  &lt;p id=&quot;j1QS&quot;&gt;Вот, например, &lt;a href=&quot;https://formik.org/&quot; target=&quot;_blank&quot;&gt;Formik&lt;/a&gt;:&lt;/p&gt;
  &lt;pre id=&quot;2mGN&quot; data-lang=&quot;tsx&quot;&gt;&amp;lt;Formik
  initialValues={{ email: &amp;quot;&amp;quot; }}
  onSubmit={(values) =&amp;gt; {
    /* ... */
  }}
&amp;gt;
  &amp;lt;Form&amp;gt;
    &amp;lt;Field name=&amp;quot;email&amp;quot; as={EmailInput} /&amp;gt;
  &amp;lt;/Form&amp;gt;
&amp;lt;/Formik&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;pOuq&quot;&gt;Если мы опечатаемся в названии поля, мы не получим ошибки ни на уровне типов, ни в рантайме:&lt;/p&gt;
  &lt;pre id=&quot;VwZR&quot; data-lang=&quot;tsx&quot;&gt;// опечатка! 👇
&amp;lt;Field name=&amp;quot;notmail&amp;quot; as={EmailInput} /&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;5rdT&quot;&gt;Вместо этого колбэк &lt;code&gt;onSubmit&lt;/code&gt; получит неожиданные значения в качестве&lt;br /&gt;аргумента.&lt;/p&gt;
  &lt;p id=&quot;7YAx&quot;&gt;Если кастомный комопнент &lt;code&gt;EmailInput&lt;/code&gt; имеет апи, не совместимый с формиком, мы также не получим ошибки ни на уровне типов, ни в рантайме — форма просто не будет работать:&lt;/p&gt;
  &lt;pre id=&quot;C6zF&quot; data-lang=&quot;tsx&quot;&gt;function EmailInput(props: {
  value: string;
  onChange: (value: string) =&amp;gt; void /* несовместимо с Formik! */;
}) {
  /* ... */
}&lt;/pre&gt;
  &lt;p id=&quot;tQpJ&quot;&gt;Так просиходит, поскольку компонент &lt;code&gt;Field&lt;/code&gt; ничего не знает о схеме формы и о&lt;br /&gt;типе поля &lt;code&gt;email&lt;/code&gt; в частности.&lt;/p&gt;
  &lt;p id=&quot;VNJ3&quot;&gt;Посмотрим на более модный &lt;a href=&quot;https://react-hook-form.com/&quot; target=&quot;_blank&quot;&gt;React Hook Form&lt;/a&gt;:&lt;/p&gt;
  &lt;pre id=&quot;Zl3E&quot; data-lang=&quot;tsx&quot;&gt;const { handleSubmit, control } = useForm({
  defaultValues: { email: &amp;quot;&amp;quot; },
});

return (
  &amp;lt;form
    onSubmit={handleSubmit((values) =&amp;gt; {
      /* ... */
    })}
  &amp;gt;
    &amp;lt;Controller
      name=&amp;quot;email&amp;quot;
      control={control}
      render={({ field }) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
    /&amp;gt;
  &amp;lt;/form&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;qGwB&quot;&gt;Дела обстоят лучше. Опечатка в названии поля будет обнаружена на уровне типов. Это благодаря хитрому типу &lt;code&gt;FieldPath&lt;/code&gt;, которым типизирован проп &lt;code&gt;name&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;9SAl&quot; data-lang=&quot;typescript&quot;&gt;type T = FieldPath&amp;lt;{ users: { email: string }[] }&amp;gt;;
// &amp;quot;users&amp;quot; | &amp;#x60;users.${number}&amp;#x60; | &amp;#x60;users.${number}.email&amp;#x60;&lt;/pre&gt;
  &lt;p id=&quot;OdmZ&quot;&gt;Совместимость кастомных контролов с библиотекой также частично гарантируется типами:&lt;/p&gt;
  &lt;pre id=&quot;HdJE&quot; data-lang=&quot;tsx&quot;&gt;function EmailInput(props: {
  value: number /* неподходящий тип */;
  onChange: (value: string) =&amp;gt; void;
}) {
  /* ... */
}

&amp;lt;Controller
  name=&amp;quot;email&amp;quot;
  control={control}
  render={({ field }) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
/&amp;gt;;
// Ошибка:
// Types of property &amp;#x27;value&amp;#x27; are incompatible.
// Type &amp;#x27;string&amp;#x27; is not assignable to type &amp;#x27;number&amp;#x27;.&lt;/pre&gt;
  &lt;p id=&quot;FVb4&quot;&gt;Но есть и недостатки. Во-первых, проверка совместимости только частичная:&lt;br /&gt;несовместимая сигантура колбэка &lt;code&gt;onChange&lt;/code&gt;, например, не приведёт к ошибке. Это на совести авторов библиотеки и вполне может быть исправлено.&lt;/p&gt;
  &lt;p id=&quot;jMGH&quot;&gt;Во-вторых, вся магия держится на информации о схеме формы, содержащейся в типе &lt;code&gt;control&lt;/code&gt;. Если &lt;code&gt;control&lt;/code&gt; передаётся неявно через контекст, проверки не работают:&lt;/p&gt;
  &lt;pre id=&quot;anga&quot; data-lang=&quot;tsx&quot;&gt;const methods = useForm({
  defaultValues: { email: &amp;quot;&amp;quot; },
});

return (
  &amp;lt;FormProvider methods={methods}&amp;gt;
    &amp;lt;form&amp;gt;
      {/* опечатка, но ошибки не будет */}
      &amp;lt;Controller
        name=&amp;quot;notmail&amp;quot;
        render={({ field }) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
      /&amp;gt;
    &amp;lt;/form&amp;gt;
  &amp;lt;/FormProvider&amp;gt;
);&lt;/pre&gt;
  &lt;p id=&quot;fhsS&quot;&gt;В-третьих, типизация пропа &lt;code&gt;name&lt;/code&gt; затрудняет разработку переиспользуемых наборов полей. Например, мы делаем переиспользуемый компонент для редактирования адресов. Мы не знаем заранее, где именно в данных формы хранится адрес, поэтому «префикс» (путь к полю с адресом) передаётся пропом.&lt;/p&gt;
  &lt;pre id=&quot;d6wl&quot; data-lang=&quot;tsx&quot;&gt;function AddressFieldset&amp;lt;T extends FieldValues&amp;gt;(props: {
  prefix: FieldPath&amp;lt;T&amp;gt;;
  control: Control&amp;lt;T&amp;gt;;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Controller
        control={props.control}
        name={&amp;#x60;${props.prefix}.city&amp;#x60;}
        render={() =&amp;gt; {
          /* ... */
        }}
      /&amp;gt;
      {/* ... */}
    &amp;lt;/&amp;gt;
  );
}
// Ошибка:
// Type &amp;#x27;&amp;#x60;${Path&amp;lt;T&amp;gt;}.city&amp;#x60;&amp;#x27; is not assignable to type &amp;#x27;Path&amp;lt;T&amp;gt;&amp;#x27;.&lt;/pre&gt;
  &lt;p id=&quot;CCR1&quot;&gt;Ожидаемо, типы не позволяют нам добавить &lt;code&gt;&amp;quot;.city&amp;quot;&lt;/code&gt; к произвольному пути и&lt;br /&gt;надеяться, что такое поле существует. Мы даже не можем быть уверены, что&lt;br /&gt;&lt;code&gt;prefix&lt;/code&gt; действительно указывает на адрес. Скорее всего, в таком случае нам&lt;br /&gt;придётся отказаться от строгой типизации совсем:&lt;/p&gt;
  &lt;pre id=&quot;1uIj&quot; data-lang=&quot;tsx&quot;&gt;function AddressFieldset(props: { prefix: string; control: Control }) {
  /* ... */
}&lt;/pre&gt;
  &lt;p id=&quot;jUS5&quot;&gt;Грустно!&lt;/p&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;BnL9&quot;&gt;Далее я опишу новую сущность — «локатор». Использование локаторов вместо строк в качестве идентификатора поля в форме (то есть в качестве значения пропа &lt;code&gt;name&lt;/code&gt;) позволит реализовать безопасные, строго типизированные формы.&lt;/p&gt;
  &lt;h2 id=&quot;VxdK&quot;&gt;Локаторы&lt;/h2&gt;
  &lt;p id=&quot;L473&quot;&gt;Локатор строится на основе схемы формы. Например:&lt;/p&gt;
  &lt;pre id=&quot;yL56&quot; data-lang=&quot;typescript&quot;&gt;type FormValues = {
  email: string;
  address: {
    city: string;
    street: string;
  };
};&lt;/pre&gt;
  &lt;p id=&quot;Jdx3&quot;&gt;Локатор для этой схемы будет выглядеть так:&lt;/p&gt;
  &lt;pre id=&quot;xMSN&quot; data-lang=&quot;typescript&quot;&gt;const typeTag = Symbol(&amp;quot;type&amp;quot;);
const pathTag = Symbol(&amp;quot;path&amp;quot;);

const locator: {
  [typeTag]?: FormValues;
  [pathTag]: string;
  email: {
    [typeTag]?: FormValues[&amp;quot;email&amp;quot;];
    [pathTag]: string;
  };
  address: {
    [typeTag]?: FormValues[&amp;quot;address&amp;quot;];
    [pathTag]: string;
    city: {
      [typeTag]?: string;
      [pathTag]: string;
    };
    street: {
      [typeTag]?: string;
      [pathTag]: string;
    };
  };
} = {
  [pathTag]: &amp;quot;&amp;quot;,
  email: {
    [pathTag]: &amp;quot;email&amp;quot;,
  },
  address: {
    [pathTag]: &amp;quot;address&amp;quot;,
    city: { [pathTag]: &amp;quot;address.city&amp;quot; },
    street: { [pathTag]: &amp;quot;address.street&amp;quot; },
  },
};&lt;/pre&gt;
  &lt;p id=&quot;N6yf&quot;&gt;Здесь тип и реализация локатора описаны вручную. Разумеется, далее мы реализуем дженерик локатор для произвольного объекта.&lt;/p&gt;
  &lt;p id=&quot;En1L&quot;&gt;Локатор имеет три важных свойства:&lt;/p&gt;
  &lt;ol id=&quot;kYzI&quot;&gt;
    &lt;li id=&quot;hvEa&quot;&gt;Локатор имеет ту же структуру, что и исходный тип.&lt;/li&gt;
    &lt;li id=&quot;Sl3K&quot;&gt;Для каждого поля исходного типа локатор хранит информацию о пути этого поля.&lt;/li&gt;
    &lt;li id=&quot;f6Cn&quot;&gt;На уровне типов, для каждого поля исходного типа локатор хранит информацию о типе этого поля.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;4VXZ&quot;&gt;Опишем локатор как дженерик тип:&lt;/p&gt;
  &lt;pre id=&quot;lOBI&quot; data-lang=&quot;typescript&quot;&gt;const typeTag = Symbol(&amp;quot;type&amp;quot;);
const pathTag = Symbol(&amp;quot;path&amp;quot;);

type Locator&amp;lt;T&amp;gt; = {
  [typeTag]?: T;
  [pathTag]: string;
} &amp;amp; (T extends Record&amp;lt;string, unknown&amp;gt;
  ? { [K in keyof T]: Locator&amp;lt;T[K]&amp;gt; }
  : T extends (infer R)[]
  ? Locator&amp;lt;R&amp;gt;[]
  : {});&lt;/pre&gt;
  &lt;p id=&quot;Z25W&quot;&gt;С точки зрения реализации, локатор — это билдер строк, в котором каждое обращение к свойству билдера добавляет название этого свойства к строке. Для реализации используем прокси:&lt;/p&gt;
  &lt;pre id=&quot;QVC6&quot; data-lang=&quot;typescript&quot;&gt;function getLocator&amp;lt;T&amp;gt;(prefix: string = &amp;quot;&amp;quot;): Locator&amp;lt;T&amp;gt; {
  return new Proxy(
    {},
    {
      get(target, key) {
        if (key === pathTag) {
          return prefix;
        }
        
        if (typeof key === &amp;quot;symbol&amp;quot;) {
          throw new Error(&amp;quot;Symbol path segments are not allowed in locator&amp;quot;);
        }
        
        const nextPrefix = prefix ? &amp;#x60;${prefix}.${key}&amp;#x60; : key;
        return getLocator(nextPrefix);
      },
    }
  ) as Locator&amp;lt;T&amp;gt;;
}&lt;/pre&gt;
  &lt;p id=&quot;UGn4&quot;&gt;Эту реализацию можно оптимизировать, переиспользуя один и тот же перехватчик &lt;code&gt;get&lt;/code&gt;, вместо того чтобы создавать каждый раз новое замыкание:&lt;/p&gt;
  &lt;pre id=&quot;Pygb&quot; data-lang=&quot;typescript&quot;&gt;const handlers: ProxyHandler&amp;lt;{ prefix: string }&amp;gt; = {
  get({ prefix }, key) {
    if (key === pathTag) {
      return prefix;
    }

    if (typeof key === &amp;quot;symbol&amp;quot;) {
      throw new Error(&amp;quot;Symbol path segments are not allowed in locator&amp;quot;);
    }

    const nextPrefix = prefix ? &amp;#x60;${prefix}.${key}&amp;#x60; : key;
    return new Proxy({ prefix: nextPrefix }, handlers);
  },
};

const rootLocator = new Proxy({ prefix: &amp;quot;&amp;quot; }, handlers);

function getLocator&amp;lt;T&amp;gt;(): Locator&amp;lt;T&amp;gt; {
  return rootLocator as unknown as Locator&amp;lt;T&amp;gt;;
}&lt;/pre&gt;
  &lt;h2 id=&quot;uNjl&quot;&gt;Использование локатора в форме&lt;/h2&gt;
  &lt;p id=&quot;iFES&quot;&gt;Апи нашей гипотетической библиотеки для работы с формами упрощённо может&lt;br /&gt;выглядеть так:&lt;/p&gt;
  &lt;pre id=&quot;lyC1&quot; data-lang=&quot;tsx&quot;&gt;function useForm&amp;lt;T&amp;gt;(options: { initialValues: T }): { n: Locator&amp;lt;T&amp;gt; } {
  return {
    n: getLocator&amp;lt;T&amp;gt;(),
    /* ... */
  };
}

function Field&amp;lt;V&amp;gt;(props: {
  name: Locator&amp;lt;V&amp;gt;;
  render: (field: {
    name: string;
    value: V;
    onChange: (value: V) =&amp;gt; void;
  }) =&amp;gt; ReactElement;
}) {
  return props.render({
    name: props.name[pathTag],
    /* ... */
  });
}&lt;/pre&gt;
  &lt;p id=&quot;9CtR&quot;&gt;Пользователь библиотеки получает корневой локатор из хука &lt;code&gt;useForm&lt;/code&gt; и использует его в качестве имени поля:&lt;/p&gt;
  &lt;pre id=&quot;1sJf&quot; data-lang=&quot;tsx&quot;&gt;const { n } = useForm({
  initialValues: {
    users: [
      {
        email: &amp;quot;&amp;quot;,
        address: {
          city: &amp;quot;&amp;quot;,
          street: &amp;quot;&amp;quot;,
        },
      },
    ],
  },
});

&amp;lt;Field
  name={n.users[0].email}
  render={(field) =&amp;gt; &amp;lt;EmailInput {...field} /&amp;gt;}
/&amp;gt;;&lt;/pre&gt;
  &lt;p id=&quot;jRRI&quot;&gt;Локатор не позволит создать имя, которого не существует в форме. Типы для пропа &lt;code&gt;render&lt;/code&gt; не позволят использовать компонент, пропы которого не совместимы с библиотекой.&lt;/p&gt;
  &lt;p id=&quot;nlpR&quot;&gt;Типизировать переиспользуемые наборы полей тоже стало проще:&lt;/p&gt;
  &lt;pre id=&quot;TDwJ&quot; data-lang=&quot;tsx&quot;&gt;function AddressFieldset(props: {
  prefix: Locator&amp;lt;{ city: string; street: string }&amp;gt;;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Field
        name={props.prefix.city}
        render={() =&amp;gt; {
          /* ... */
        }}
      /&amp;gt;
      {/* ... */}
    &amp;lt;/&amp;gt;
  );
}

&amp;lt;AddressFieldset prefix={n.users[0].address} /&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;AaFQ&quot;&gt;Этот компонент требует, чтобы ему передали локатор, указывающий именно на адрес, и поэтому может безопасно обращаться к полям адреса через переданный локатор. И, поскольку локатор повторяет структуру исходного объекта, вместо локатора адреса можно передать локатор любого объекта, совместимого с адресом по присваиванию.&lt;/p&gt;
  &lt;p id=&quot;Wg1i&quot;&gt;И, кстати, рефакторинг на названиях полей теперь работает.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(236, 74%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;Bfu0&quot;&gt;&lt;strong&gt;Защита от чужого локатора&lt;/strong&gt;&lt;/p&gt;
    &lt;p id=&quot;57kF&quot;&gt;Мы не должны разрешать использовать локатор одной формы для полей другой формы.&lt;/p&gt;
    &lt;pre id=&quot;dbot&quot; data-lang=&quot;tsx&quot;&gt;const { form, n } = useForm({
  initialValues: { email: &amp;#x27;&amp;#x27; }
});

const { form: anotherForm, n: anotherN } = useForm({
  initialValues: { notemail: &amp;#x27;&amp;#x27; }
});

&amp;lt;FormProvider form={anotherForm}&amp;gt;
  {/* плохо: в anotherForm нет email */}
  &amp;lt;Field name={n.email} render={() =&amp;gt; {
    /* ... */
  }} /&amp;gt;
&amp;lt;/FormProvider&amp;gt;&lt;/pre&gt;
    &lt;p id=&quot;e3CT&quot;&gt;Защититься от использования чужого локатора мы можем только в рантайме. Подход такой: пометим саму форму и её корневой локатор уникальным значением — «брендом»; производные локаторы наследуют бренд от своего родителя; компонент &lt;code&gt;Field&lt;/code&gt; проверяет совпадение бренда формы и бренда локатора.&lt;/p&gt;
    &lt;pre id=&quot;Uagd&quot; data-lang=&quot;typescript&quot;&gt;function useForm&amp;lt;T&amp;gt;(options: { initialValues: T }) {
  const [brand] = useState(() =&amp;gt; Symbol(&amp;#x27;formBrand&amp;#x27;));

  return {
    // локатор n и все производные от него будут помечены
    // брендом, уникальным для этой формы
    n: getLocator&amp;lt;T&amp;gt;(brand),

    // тот же самый бренд будет и в контексте формы
    form: { brand, /* ... */ },
  };
}

function Field&amp;lt;V&amp;gt;(props: {
  name: Locator&amp;lt;V&amp;gt;;
  /* ... */
}) {
  const { brand } = useContext(FormContext);

  // сверям бренд из контекста формы и бренд локатора
  if (brand !== name[brandTag]) {
    throw new Error(&amp;#x27;Foreign locator used to address form field&amp;#x27;);
  }

  /* ... */
}&lt;/pre&gt;
    &lt;p id=&quot;tLjz&quot;&gt;Жаль, что нельзя защититься от чужих локаторов на уровне типов, но контексты вообще плохо поддаются строгой типизации.&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;Byv4&quot;&gt;Заключение&lt;/h2&gt;
  &lt;p id=&quot;rPZX&quot;&gt;Однажды кто-то, кому менее лень, использует описанные принципы для создания по-настоящему типобезопасной библиотеки для работы с формами.&lt;/p&gt;

</content></entry><entry><id>smalldogenergy:font-metrics</id><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy/font-metrics?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><title>Контейнер обнимает текст</title><published>2022-09-04T01:16:23.241Z</published><updated>2022-09-05T00:12:41.933Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/3d/f8/3df86d68-6d6a-4637-862c-79220d03393f.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img4.teletype.in/files/3a/4b/3a4bf573-4df0-4371-a0ad-23ba1a8759b5.png&quot;&gt;Про странности работы с размером шрифта и высотой строки на вебе уже писали, и лучше моего. Но зря я что ли сам разбирался! Плюс, я принёс практические решения. Так что давайте ещё разок.</summary><content type="html">
  &lt;p id=&quot;VtQR&quot;&gt;Про странности работы с размером шрифта и высотой строки на вебе &lt;a href=&quot;#ZEOO&quot;&gt;уже писали&lt;/a&gt;, и лучше моего. Но зря я что ли сам разбирался! Плюс, я принёс практические решения. Так что давайте ещё разок.&lt;/p&gt;
  &lt;h2 id=&quot;9HUY&quot;&gt;Метрики шрифта&lt;/h2&gt;
  &lt;p id=&quot;8Hd6&quot;&gt;Коротко, файл со шрифтом содержит несколько важных циферок. Это метрики шрифта:&lt;/p&gt;
  &lt;ol id=&quot;cIaJ&quot;&gt;
    &lt;li id=&quot;xImw&quot;&gt;Размер em-квадрата. Это произвольная область, задающая масштаб системы координат для знаков внутри шрифта.&lt;/li&gt;
    &lt;li id=&quot;V7xy&quot;&gt;Положение базовой линии. Базовая линия задаёт начало координат. По договорённости, буквы без нижних выносных элементов «стоят» прямо на базовой линии.&lt;/li&gt;
    &lt;li id=&quot;bmd2&quot;&gt;Высота заглавных букв (cap height). Если автор шрифта не накосячил, это действительно высота заглавных букв без выносных элементов.&lt;/li&gt;
    &lt;li id=&quot;XRSR&quot;&gt;Высота строчных букв (x height) — аналогично, буквально высота буквы „x“.&lt;/li&gt;
    &lt;li id=&quot;dkEr&quot;&gt;Ascender и descender. Показывают, сколько места давать буквам сверху и снизу от базовой линии. В сумме они задают «естественную» высоту строки для шрифта.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;figure id=&quot;cBSa&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/3a/4b/3a4bf573-4df0-4371-a0ad-23ba1a8759b5.png&quot; width=&quot;1364&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;blJK&quot;&gt;Например, у шрифта &lt;em&gt;Source Sans 3&lt;/em&gt; такие метрики:&lt;/p&gt;
  &lt;ol id=&quot;DZWF&quot;&gt;
    &lt;li id=&quot;8DGF&quot;&gt;Размер em-квадрата: 1000 единиц&lt;/li&gt;
    &lt;li id=&quot;r0zC&quot;&gt;Базовая линия: 200 единиц от нижней стороны квадрата (и, соответственно, 800 единиц от верхней)&lt;/li&gt;
    &lt;li id=&quot;Vr7j&quot;&gt;Высота заглавных: 660 единиц&lt;/li&gt;
    &lt;li id=&quot;mmRd&quot;&gt;Высота строчных: 478 единиц&lt;/li&gt;
    &lt;li id=&quot;84fF&quot;&gt;Ascender: 1024, descender: 400, в сумме 1424 единицы.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;section style=&quot;background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;UHrE&quot;&gt;— Петька, cap height!&lt;br /&gt;— 660!&lt;br /&gt;— Что 660?&lt;br /&gt;— А что cap height?&lt;/p&gt;
    &lt;p id=&quot;xioK&quot;&gt;Шрифты векторные, так что никаких абсолютных единиц, вроде пикселей, внутри самого шрифта нет. Есть только сетка единиц, задаваемая em-квадратом и базовой линией.&lt;/p&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;9LQZ&quot;&gt;Что мы имеем в виду, когда говорим font-size&lt;/h2&gt;
  &lt;p id=&quot;epKy&quot;&gt;Указывая в CSS размер шрифта в пикселях (например, &lt;code&gt;font-size: 100px&lt;/code&gt;), мы на самом деле задаём абсолютный размер em-квадрата. Собственно, поэтому текущий размер шрифта в CSS — это &lt;code&gt;1em&lt;/code&gt;.&lt;/p&gt;
  &lt;figure id=&quot;4n3s&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/5a/bd/5abdb7d6-0554-49f3-ba18-fe33ccd250db.png&quot; width=&quot;1364&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;BbOX&quot;&gt;Немного арифметики. У &lt;em&gt;Source Sans 3 &lt;/em&gt;em-квадрат на 1000 единиц, так что при &lt;code&gt;font-size: 100px&lt;/code&gt; одна единица в координатах шрифта в реальности занимает на экране 0,1 пиксель. Заглавные буквы высотой 660 единиц имеют реальную высоту 66 пикселей. А &lt;code&gt;span&lt;/code&gt; с таким размером шрифта имеет высоту 142,4 пикселя (с округлением до целого физического пикселя).&lt;/p&gt;
  &lt;p id=&quot;i7Tm&quot;&gt;Em-квадрат — относительно произвольная область, не видимая невооружённым взглядом. В зависимости от того, как она задана, разные шрифты будут выглядеть крупнее или мельче при том же размере шрифта.&lt;/p&gt;
  &lt;figure id=&quot;JORQ&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f6/fb/f6fbf0f4-4038-47a1-89d8-5771694c2d2e.png&quot; width=&quot;1561&quot; /&gt;
    &lt;figcaption&gt;По возрастанию: &lt;em&gt;Crimson Pro, Source Sans 3, Roboto&lt;/em&gt; с одним и тем же размером шрифта имеют разный визуальный размер.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;50vh&quot;&gt;При сочетании разных шрифтов на одной странице такая непредсказуемость становится проблемой.&lt;/p&gt;
  &lt;h2 id=&quot;5ae3&quot;&gt;Делаем размер шрифта предсказуемым&lt;/h2&gt;
  &lt;p id=&quot;z5qm&quot;&gt;Что считать настоящим визуальным размером шрифта: высоту строчных (x height) или высоту заглавных (cap height)? В интерфейсе, если надо выровнять текст с иконкой, я обычно ориентируюсь на заглавные; а в журнальной вёрстке сочетают шрифты с одинаковой высотой строчных.&lt;/p&gt;
  &lt;p id=&quot;q6ME&quot;&gt;Для себя я выбрал высоту заглавных как визуальную метрику размера шрифта, а если вам это не подходит — просто замените везде дальше cap height на x height.&lt;/p&gt;
  &lt;p id=&quot;AJnk&quot;&gt;Зная размер em-квадрата и cap height, посчитаем, какой указывать &lt;code&gt;font-size&lt;/code&gt;, чтобы получить желаемую высоту заглавных:&lt;/p&gt;
  &lt;p id=&quot;yaFP&quot; data-align=&quot;center&quot;&gt;размер шрифта &lt;strong&gt;=&lt;/strong&gt; желаемая высота заглавных &lt;strong&gt;÷&lt;/strong&gt; высота заглавных в em&lt;br /&gt;высота заглавных в em &lt;strong&gt;=&lt;/strong&gt; высота заглавных &lt;strong&gt;÷&lt;/strong&gt; размер em-квардрата&lt;/p&gt;
  &lt;p id=&quot;Buy9&quot;&gt;Например, хотим иметь одну высоту заглавных в 72 пикселя для трёх разных шрифтов:&lt;/p&gt;
  &lt;p id=&quot;A6vC&quot; data-align=&quot;center&quot;&gt;&lt;em&gt;Source Sans 3:&lt;/em&gt; 72px ÷ (660 / 1000) ≈ 109px&lt;br /&gt;&lt;em&gt;Crimson Pro: &lt;/em&gt;72px ÷ (587 / 1024) ≈ 126px&lt;br /&gt;&lt;em&gt;Roboto: &lt;/em&gt;72px ÷ (1456 / 2048) ≈ 101px.&lt;/p&gt;
  &lt;figure id=&quot;FQSc&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/db/dc/dbdc2324-a6c2-49f7-a1a8-33b3f4421dd7.png&quot; width=&quot;1588&quot; /&gt;
    &lt;figcaption&gt;Равенство, братство: &lt;em&gt;Crimson Pro&lt;/em&gt; с размером 126 пикселей, &lt;em&gt;Source Sans 3&lt;/em&gt; с размером 109 пикселей, &lt;em&gt;Roboto&lt;/em&gt; с размером 101 пиксель.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;LNiC&quot;&gt;На практике, чтобы не считать руками каждый раз, заведём несколько вспомогательных стилей:&lt;/p&gt;
  &lt;pre id=&quot;4d6k&quot; data-lang=&quot;css&quot;&gt;:root {
  font-family: &amp;#x27;Source Sans 3&amp;#x27;;
  --cap-height-em: 0.66; /* = 660 / 1000 */
}

/* Typography helper */
.tyh {
  font-size: calc(var(--cap-height) / var(--cap-height-em));
}&lt;/pre&gt;
  &lt;p id=&quot;UqUE&quot;&gt;Используя наш класс-хелпер, мы теперь можем где угодно задавать размер шрифта, ориентируясь на высоту заглавных:&lt;/p&gt;
  &lt;pre id=&quot;gRMJ&quot; data-lang=&quot;html&quot;&gt;&amp;lt;p class=&amp;quot;tyh&amp;quot; style=&amp;quot;--cap-height: 72px&amp;quot;&amp;gt;
  Hello, world!
&amp;lt;/p&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;npKz&quot;&gt;Предсказуемость!&lt;/p&gt;
  &lt;h2 id=&quot;njrS&quot;&gt;Теперь про высоту строки&lt;/h2&gt;
  &lt;p id=&quot;VsKD&quot;&gt;В метриках неявно зашита «естественная» высота строки для этого шрифта, равная сумме ascender + descender. Указывая в CSS явный &lt;code&gt;line-height&lt;/code&gt;, мы просим браузер докинуть отступов к этой высоте. Эти дополнительные отступы, которые называются leading — читается [лэдинг], — распределяются поровну сверху и снизу:&lt;/p&gt;
  &lt;figure id=&quot;Oacu&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8e/e2/8ee24373-0c09-41f5-ad5f-0b2a9ac53eab.png&quot; width=&quot;1364&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;hAhs&quot;&gt;Вертикальное положение букв в этой области зависит от отношения ascender к descender. Ещё немного арифметики: возьмём &lt;em&gt;Source Sans 3&lt;/em&gt; в блоке с &lt;code&gt;font-size: 100px&lt;/code&gt; и &lt;code&gt;line-height: 150px&lt;/code&gt;. Естественная высота строки при таком размере шрифта равна 142,4 пикселя (округлим до 142). Значит, браузер добавит к блоку 4 дополнительных пикселя сверху и снизу.&lt;/p&gt;
  &lt;p id=&quot;lES4&quot;&gt;При этом расстояние от нижнего края блока до базовой линии составит&lt;/p&gt;
  &lt;p id=&quot;uv06&quot; data-align=&quot;center&quot;&gt;descender &lt;strong&gt;+&lt;/strong&gt; ½ leading &lt;strong&gt;=&lt;/strong&gt;&lt;br /&gt;40 + 4 = 44px,&lt;/p&gt;
  &lt;p id=&quot;oXlv&quot;&gt;а от верхнего края до верхнего края заглавных —&lt;/p&gt;
  &lt;p id=&quot;ibyv&quot; data-align=&quot;center&quot;&gt;ascender &lt;strong&gt;+&lt;/strong&gt; ½ leading &lt;strong&gt;−&lt;/strong&gt; cap height &lt;strong&gt;=&lt;/strong&gt;&lt;br /&gt;102,4 + 4 − 66 ≈ 40px.&lt;/p&gt;
  &lt;p id=&quot;vhB5&quot;&gt;Получается, заглавные буквы оказываются не строго в центре области, выделенной под строку. Это тоже головная боль, например, в интерфейсах, когда иконка и текст в блоках равной высоты оказываются смещены относительно друг друга.&lt;/p&gt;
  &lt;h2 id=&quot;5mG4&quot;&gt;Контейнер обнимает текст&lt;/h2&gt;
  &lt;p id=&quot;fnuk&quot;&gt;В идеальном мире, свойство &lt;code&gt;line-height&lt;/code&gt; устанавливает &lt;em&gt;только&lt;/em&gt; расстояние между базовыми линиями строк текста, не добавляя дополнительных отступов над первой и под последней строкой.&lt;/p&gt;
  &lt;p id=&quot;NgW3&quot;&gt;В ещё более идеальном мире вертикальное положение текста в блоке вообще не зависит от таких произвольных метрик, как ascender и descender. Для взаимного выравнивания блоков с текстом удобнее всего, если верхний край блока прижимается к верхнему краю заглавных, а нижний — к базовой линии.&lt;/p&gt;
  &lt;figure id=&quot;JDeu&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/0e/85/0e855aa3-5398-4840-826b-928e8c9cbd87.png&quot; width=&quot;1364&quot; /&gt;
    &lt;figcaption&gt;Границы текстового блока в идеальном мире.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;e4I0&quot;&gt;Научим наш класс &lt;code&gt;.tyh&lt;/code&gt; производить все необходимые расчёты:&lt;/p&gt;
  &lt;pre id=&quot;rGpu&quot; data-lang=&quot;css&quot;&gt;:root {
  font-family: &amp;#x27;Source Sans 3&amp;#x27;;
  --cap-height-em: 0.66; /* = 660 / 1000 */
  --ascender-em: 1.024; /* = 1024 / 1000 */
  --descender-em: 0.4; /* = 400 / 1000 */
}

/* Typography helper */
.tyh {
  /* Коэффициент для перевода em в абсолютные величины,
     а заодно и значение font-size: */
  --units-per-em: calc(var(--cap-height) / var(--cap-height-em));

  --half-leading: calc((
    var(--line-spacing) -
    (var(--ascender-em) + var(--descender-em)) * var(--units-per-em)
  ) / 2);

  --trim-top: calc(-1 * (
    var(--half-leading) +
    var(--ascender-em) * var(--units-per-em) -
    var(--cap-height)
  ));

  --trim-bottom: calc(-1 * (
    var(--half-leading) +
    var(--descender-em) * var(--units-per-em)
  ));
  
  font-size: var(--units-per-em);
  line-height: var(--line-spacing);
}&lt;/pre&gt;
  &lt;p id=&quot;16Ry&quot;&gt;Осталось добавить блоку отрицательные поля, откусывающие &lt;code&gt;--trim-top&lt;/code&gt; сверху и &lt;code&gt;--trim-bottom&lt;/code&gt; снизу. Чтобы избежать проблем с margin collapsing, добавим их не самому блоку, а псевдоэлементам в начале и конце блока:&lt;/p&gt;
  &lt;pre id=&quot;rBx6&quot; data-lang=&quot;css&quot;&gt;.th::before,
.th::after {
  content: &amp;quot;&amp;quot;;
  display: table;
}

.th::before {
  margin-bottom: var(--trim-top);
}

.th::after {
  margin-top: var(--trim-bottom);
}&lt;/pre&gt;
  &lt;p id=&quot;nIli&quot;&gt;Готово:&lt;/p&gt;
  &lt;pre id=&quot;GiJ8&quot; data-lang=&quot;html&quot;&gt;&amp;lt;p class=&amp;quot;tyh&amp;quot; style=&amp;quot;--cap-height: 72px; --line-spacing: 100px;&amp;quot;&amp;gt;
  The quick brown fox jumps
&amp;lt;/p&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;aErI&quot;&gt;А вот и &lt;a href=&quot;https://codepen.io/WhiskerFatigue/pen/yLjyXPG&quot; target=&quot;_blank&quot;&gt;демка&lt;/a&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;Impa&quot;&gt;Зачем всё это было, и что делать дальше&lt;/h2&gt;
  &lt;p id=&quot;oQOb&quot;&gt;Используя класс &lt;code&gt;.tyh&lt;/code&gt;, гораздо проще подгонять текстовые блоки друг к другу, к иллюстрациям и к иконкам. Отдельно замечу, что заодно стало куда легче следить за &lt;a href=&quot;https://nobelfaik.livejournal.com/148110.html&quot; target=&quot;_blank&quot;&gt;приводностью вёрстки&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;77vB&quot;&gt;При этом я не считаю, что стоит тащить такой CSS везде. Во-первых, вам придётся объяснить своим коллегам всё то, что я попытался объяснить в этой статье, а это уже не тривиальная задача. Во-вторых, кому-то придётся следить за актуальностью захардкоженных метрик в коде, либо придумывать пайплайн, чтобы автоматизировать их генерацию. В-третьих, возможно, такой перфекционизм просто никому не нужен.&lt;/p&gt;
  &lt;p id=&quot;0Lr0&quot;&gt;Но если вы всё же решите заморочиться, метрики шрифта можно подсмотреть в FontForge (полезнейшее приложение с ужасным, ужасным интерфейсом), либо достать программно библиотеками вроде &lt;a href=&quot;https://github.com/foliojs/fontkit&quot; target=&quot;_blank&quot;&gt;fontkit&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;PnWc&quot;&gt;Наконец, рекомендую прочитать прекрасные статьи из следующего раздела. Там особенно интересно, откуда взялись все эти странные термины, вроде em square и leading.&lt;/p&gt;
  &lt;h2 id=&quot;ZEOO&quot;&gt;Литература&lt;/h2&gt;
  &lt;p id=&quot;4Ng5&quot;&gt;Три статьи, описывающие предметную область лучше, чем я:&lt;/p&gt;
  &lt;p id=&quot;uolw&quot;&gt;&lt;a href=&quot;https://tonsky.me/blog/font-size/&quot; target=&quot;_blank&quot;&gt;Font size is useless; let’s fix it&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align&quot; target=&quot;_blank&quot;&gt;Deep dive CSS: font metrics, line-height and vertical-align&lt;br /&gt;&lt;/a&gt;&lt;a href=&quot;https://www.figma.com/blog/line-height-changes/&quot; target=&quot;_blank&quot;&gt;Getting to the bottom of line height in Figma&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;37AZ&quot;&gt;Основное вдохновение для класса &lt;code&gt;.tyh&lt;/code&gt; — библиотека &lt;a href=&quot;https://seek-oss.github.io/capsize/&quot; target=&quot;_blank&quot;&gt;Capsize&lt;/a&gt;.&lt;/p&gt;

</content></entry><entry><id>smalldogenergy:this-time-tomorrow</id><link rel="alternate" type="text/html" href="https://teletype.in/@smalldogenergy/this-time-tomorrow?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=smalldogenergy"></link><title>Завтра в это же время: на удивление сложная реализация на JavaScript</title><published>2022-05-22T02:36:24.703Z</published><updated>2022-05-22T14:59:45.819Z</updated><summary type="html">Задача: имея юникстайм и название часового пояса, получить юникстайм этого же времени дня на следующий день.</summary><content type="html">
  &lt;p id=&quot;6PIn&quot;&gt;Задача: имея юникстайм и название часового пояса, получить юникстайм этого же времени дня на следующий день.&lt;/p&gt;
  &lt;p id=&quot;whf8&quot;&gt;Например: 1648292400 Europe/Prague (26 марта 2022, 12:00 по местному времени). Хотим получить 1648375200 Europe/Prague (27 марта 2022, 12:00 по местному времени).&lt;/p&gt;
  &lt;p id=&quot;GocL&quot;&gt;Решение на JavaScript: на удивление сложное.&lt;/p&gt;
  &lt;h2 id=&quot;_1&quot;&gt;Первая наивная попытка&lt;/h2&gt;
  &lt;p id=&quot;OIiv&quot;&gt;Добавим 24 часа к исходному таймстэмпу:&lt;/p&gt;
  &lt;pre id=&quot;2g6A&quot; data-lang=&quot;javascript&quot;&gt;const timestamp = 1648292400;
const timeZone = &amp;quot;Europe/Prague&amp;quot;;
const thisTimeTomorrow = timestamp + 24 * 60 * 60;
console.log(thisTimeTomorrow, timeZone);
// 1648378800 Europe/Prague&lt;/pre&gt;
  &lt;p id=&quot;E2ue&quot;&gt;На удивление, полученный тайстэмп соотвествует не 12:00, а 13:00 по местному времени. По совпадению, именно в ночь с 26 на 27 марта в Чехии переводят часы на летнее время — на час вперёд. Как результат, полдень 26 и 27 марта отделяет всего 23, а не 24 часа.&lt;/p&gt;
  &lt;h2 id=&quot;_2&quot;&gt;Вторая наивная попытка&lt;/h2&gt;
  &lt;p id=&quot;x1cy&quot;&gt;Не будем работать с датой и временем самостоятельно, доверимся классу &lt;code&gt;Date&lt;/code&gt; стандартной библиотеки:&lt;/p&gt;
  &lt;pre id=&quot;0yq7&quot; data-lang=&quot;javascript&quot;&gt;const timestamp = 1648292400;
const date = new Date(timestamp * 1000);
date.setDate(date.getDate() + 1);
console.log(date);
console.log(Math.floor(date / 1000));&lt;/pre&gt;
  &lt;p id=&quot;VCZU&quot;&gt;Этот код прекрасно работает, если системный часовой пояс — Europe/Prague:&lt;/p&gt;
  &lt;pre id=&quot;J5XW&quot;&gt;Sun Mar 27 2022 12:00:00 GMT+0200 (Central European Summer Time)
1648375200&lt;/pre&gt;
  &lt;p id=&quot;lV3H&quot;&gt;(В этом легко убедиться, исполнив код в консоли Chrome. Предварительно в панели Sensors надо переопределить текущее местоположение и указать соответствующий Timezone ID.)&lt;/p&gt;
  &lt;p id=&quot;FB6y&quot;&gt;Если же код работает на серверах, которые принято держать в UTC, получим другой результат:&lt;/p&gt;
  &lt;pre id=&quot;k5FH&quot;&gt;Sun Mar 27 2022 11:00:00 GMT+0000 (Coordinated Universal Time)
1648378800&lt;/pre&gt;
  &lt;p id=&quot;27HV&quot;&gt;Этот ошибочный результат ожидаемо совпадает с первой наивной попыткой.&lt;/p&gt;
  &lt;h2 id=&quot;date&quot;&gt;Проблема с Date&lt;/h2&gt;
  &lt;p id=&quot;hjvN&quot;&gt;Класс &lt;code&gt;Date&lt;/code&gt; умеет работать только с двумя часовыми поясами: системным и UTC. Методы &lt;code&gt;getDate&lt;/code&gt;, &lt;code&gt;getHours&lt;/code&gt;, &lt;code&gt;toString&lt;/code&gt; и другие интерпретируют таймстэмп по местному времени, а их аналоги &lt;code&gt;getUTCDate&lt;/code&gt;, &lt;code&gt;getUTCHours&lt;/code&gt;, &lt;code&gt;toUTCString&lt;/code&gt; — по UTC.&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(199, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;hwRv&quot;&gt;(Стилистические вопросы написания аббревиатур в идентификаторах &lt;s&gt;выходят&lt;/s&gt; выходили бы за рамки этой статьи, если бы ответ не был прост: всегда &lt;code&gt;getUtcDate&lt;/code&gt;. Впрочем, после &lt;code&gt;XMLHttpRequest&lt;/code&gt; наши ожидания не высоки.)&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;e8aX&quot;&gt;В этом огромная слабость API для работы с датой и временем в JavaScript. Попытка закрыть этот недочёт предпринята в новом API &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;&lt;/a&gt;, который позволяет интерпретировать таймстэмп в любом часовом поясе:&lt;/p&gt;
  &lt;pre id=&quot;nISg&quot; data-lang=&quot;javascript&quot;&gt;const formatter = new Intl.DateTimeFormat(&amp;quot;en-US&amp;quot;, {
  timeZone: &amp;quot;Europe/Prague&amp;quot;,
  dateStyle: &amp;quot;long&amp;quot;,
  timeStyle: &amp;quot;long&amp;quot;,
});

console.log(formatter.format(new Date(1648292400 * 1000)));
// March 26, 2022 at 12:00:00 PM GMT+1

console.log(formatter.format(new Date(1648375200 * 1000)));
// March 27, 2022 at 12:00:00 PM GMT+2&lt;/pre&gt;
  &lt;p id=&quot;KlEV&quot;&gt;Однако, говоря «интерпретировать таймстэмп», мы подразумеваем две задачи:&lt;/p&gt;
  &lt;ol id=&quot;CzvK&quot;&gt;
    &lt;li id=&quot;TwOI&quot;&gt;Отформатировать числовой таймстэмп как строку в заданном часовом поясе&lt;/li&gt;
    &lt;li id=&quot;Npdm&quot;&gt;Прочитать строку с датой и временем как таймстэмп в заданном часовом поясе&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;A7Iy&quot;&gt;&lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; решает первую задачу. Для второй задачи в JavaScript по-прежнему не существует простого решения.&lt;/p&gt;
  &lt;h2 id=&quot;_3&quot;&gt;Третья попытка&lt;/h2&gt;
  &lt;p id=&quot;Nwjn&quot;&gt;Несмотря на свою неполноту, &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; приближает нас к решению задачи, поскольку даёт доступ к ключевой информации: какой временной сдвиг действует в данный момент в данном часовом поясе.&lt;/p&gt;
  &lt;p id=&quot;mPQM&quot;&gt;Извлечём эту информацию:&lt;/p&gt;
  &lt;pre id=&quot;ZZmS&quot; data-lang=&quot;javascript&quot;&gt;const offsetPattern = /GMT([+-]\d+)/;

function getTimeZoneOffset(timestamp, timeZone) {
  const formatter = new Intl.DateTimeFormat(&amp;quot;en-US&amp;quot;, {
    timeStyle: &amp;quot;long&amp;quot;,
    timeZone,
  });
  const localTimeStr = formatter.format(new Date(timestamp * 1000));
  const offsetMatch = localTimeStr.match(offsetPattern);
  const offsetSecs = Number.parseInt(offsetMatch[1]) * 60 * 60;
  return offsetSecs;
}

console.log(getTimeZoneOffset(1648292400, &amp;quot;Europe/Prague&amp;quot;));
// 3600

console.log(getTimeZoneOffset(1648375200, &amp;quot;Europe/Prague&amp;quot;));
// 7200&lt;/pre&gt;
  &lt;p id=&quot;j2iQ&quot;&gt;Используя эту вспомогательную функцию, обновим наше первое наивное решение:&lt;/p&gt;
  &lt;pre id=&quot;JW5J&quot; data-lang=&quot;javascript&quot;&gt;const timestamp = 1648292400;
const timeZone = &amp;quot;Europe/Prague&amp;quot;;
const tomorrow = timestamp + 24 * 60 * 60;
const thisTimeTomorrow =
  tomorrow +
  getTimeZoneOffset(timestamp, timeZone) -
  getTimeZoneOffset(tomorrow, timeZone);
console.log(thisTimeTomorrow, timeZone);
// 1648375200 Europe/Prague&lt;/pre&gt;
  &lt;p id=&quot;mbaW&quot;&gt;И это уже похоже на правду.&lt;/p&gt;
  &lt;h2 id=&quot;_4&quot;&gt;Заключительные примечания&lt;/h2&gt;
  &lt;p id=&quot;lbib&quot;&gt;Во-первых, наше решение ломается на 1648258200 Europe/Prague (26 марта 2022, 2:30 по местному времени) — мы получим 1648341000 Europe/Prague (27 марта, 01:30 по местному времени). Впрочем, не понятно, что значит «завтра в это же время» в таком случае: стрелки переводятся вперёд в два часа ночи, так что 02:30 просто не существует 27 марта.&lt;/p&gt;
  &lt;p id=&quot;kgOT&quot;&gt;Во-вторых, естественно, эта задача уже решена в библиотеках. Например, с использованием &lt;code&gt;date-fns-tz&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;iaUC&quot; data-lang=&quot;javascript&quot;&gt;const thisTimeTomorrow = zonedTimeToUtc(
  addDays(utcToZonedTime(timestamp, timeZone), 1),
  timeZone
);&lt;/pre&gt;
  &lt;p id=&quot;ex9H&quot;&gt;В-третьих, эта задача хорошо показывает, почему недостаточно хранить только временной сдвиг часового пояса вместе с тайстэмпом. Всегда храните название часового пояса по IANA.&lt;/p&gt;
  &lt;p id=&quot;IXlo&quot;&gt;В-четвёртых, страны иногда меняют правила перехода на летнее время и временные сдвиги часовых поясов. Не факт, что ваш рантайм своевременно отразит эти изменения. Например, Node.js бандлит &lt;a href=&quot;https://icu.unicode.org/&quot; target=&quot;_blank&quot;&gt;библиотеку ICU&lt;/a&gt;, содержащую эту информацию. Не обновив Node.js, вы не получите актуальную информацию о часовых поясах.&lt;/p&gt;
  &lt;p id=&quot;TCvb&quot;&gt;Если эта информация критична для работы вашего приложения, пересоберите Node.js с флагом &lt;a href=&quot;https://nodejs.org/api/intl.html#build-with-a-pre-installed-icu-system-icu&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;--with-intl=system-icu&lt;/code&gt;&lt;/a&gt; и своевременно обновляйте библиотеку ICU в вашей системе.&lt;/p&gt;
  &lt;h2 id=&quot;_5&quot;&gt;Вывод&lt;/h2&gt;
  &lt;p id=&quot;YfIe&quot;&gt;Не завидую тем, кому приходится работать с часовыми поясами.&lt;/p&gt;

</content></entry></feed>