my blog https://overreacted.io

Compare changes

Choose any two refs to compare.

Changed files
+861 -218
app
public
a-complete-guide-to-useeffect
hire-me-in-japan
how-imports-work-in-rsc
how-to-fix-any-bug
impossible-components
introducing-rsc-explorer
open-social
react-as-a-ui-runtime
the-math-is-haunted
the-two-reacts
where-its-at
+4 -3
app/Link.tsx
··· 27 27 ...rest 28 28 }: { 29 29 className?: string; 30 - children: React.ReactNode; 30 + children?: React.ReactNode; 31 31 style?: React.CSSProperties; 32 32 href: string; 33 33 target?: string; 34 34 } & React.ComponentProps<typeof NextLink>) { 35 35 const router = useRouter(); 36 36 const [isNavigating, trackNavigation] = useTransition(); 37 - if (!target && !href.startsWith("/") && !href.startsWith("#")) { 37 + const isExternal = /^https?:\/\//.test(href); 38 + if (!target && isExternal) { 38 39 target = "_blank"; 39 40 } 40 41 return ( ··· 50 51 }); 51 52 } 52 53 }} 53 - className={[className, `scale-100 active:scale-100`].join(" ")} 54 + className={[className, "scale-100 active:scale-100"].join(" ")} 54 55 style={{ 55 56 ...style, 56 57 transform: isNavigating ? "scale(1)" : "",
+16
app/TextLink.tsx
··· 1 + import Link from "./Link"; 2 + 3 + export default function TextLink({ 4 + className, 5 + ...props 6 + }: React.ComponentProps<typeof Link>) { 7 + return ( 8 + <Link 9 + {...props} 10 + className={[ 11 + "underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]", 12 + className, 13 + ].join(" ")} 14 + /> 15 + ); 16 + }
+1 -1
app/[slug]/layout.tsx
··· 4 4 return ( 5 5 <> 6 6 {children} 7 - <footer className="mt-12"> 7 + <footer className="mt-20"> 8 8 <HomeLink /> 9 9 </footer> 10 10 </>
+50 -141
app/[slug]/markdown.css
··· 1 + /* CSS vars for code block customization (used by Wrapper components) */ 1 2 .markdown { 2 - line-height: 28px; 3 - --path: none; 4 - --radius-top: 12px; 5 - --radius-bottom: 12px; 6 - --padding-top: 1rem; 7 - --padding-bottom: 1rem; 8 - } 9 - 10 - .markdown p { 11 - @apply pb-8; 12 - } 13 - 14 - .markdown a:not(.tip):not(.linked-heading) { 15 - @apply border-b-[1px] border-[--link] text-[--link]; 16 - } 17 - 18 - .markdown hr { 19 - @apply pt-8 opacity-60 dark:opacity-10; 20 - } 21 - 22 - .markdown h2 { 23 - @apply mt-2 pb-8 text-3xl font-bold; 24 - } 25 - 26 - .markdown h3 { 27 - @apply mt-2 pb-8 text-2xl font-bold; 28 - } 29 - 30 - .markdown h4 { 31 - @apply mt-2 pb-8 text-xl font-bold; 32 - } 33 - 34 - .markdown :is(h1, h2, h3, h4) a:is(:hover, :focus, :active)::before, 35 - .markdown :is(h1, h2, h3, h4):is(:target, :focus) a::before { 36 - content: "#"; 37 - position: absolute; 38 - transform: translate(-1em); 39 - opacity: 0.7; 40 - } 41 - 42 - .markdown :not(pre) > code { 43 - border-radius: 10px; 44 - background: var(--inlineCode-bg); 45 - color: var(--inlineCode-text); 46 - padding: 0.15em 0.2em 0.05em; 47 - white-space: normal; 48 - } 49 - 50 - .markdown pre { 51 - @apply -mx-4 mb-8 overflow-y-auto p-4 text-sm; 52 - clip-path: var(--path); 53 - border-top-right-radius: var(--radius-top); 54 - border-top-left-radius: var(--radius-top); 55 - border-bottom-right-radius: var(--radius-bottom); 56 - border-bottom-left-radius: var(--radius-bottom); 57 - padding-top: var(--padding-top); 58 - padding-bottom: var(--padding-bottom); 59 - } 60 - 61 - .markdown pre code { 62 - width: auto; 63 - } 64 - 65 - .markdown blockquote { 66 - @apply relative -left-2 -ml-4 mb-8 pl-4; 67 - font-style: italic; 68 - border-left: 3px solid hsla(0, 0%, 0%, 0.9); 69 - border-left-color: inherit; 70 - opacity: 0.8; 71 - } 72 - 73 - .markdown blockquote p { 74 - margin: 0; 75 - padding: 0; 76 - } 77 - 78 - .markdown p img { 79 - margin-bottom: 0; 80 - } 81 - 82 - .markdown ul:not(.unstyled) { 83 - @apply list-inside md:list-outside list-disc; 84 - margin-top: 0; 85 - padding-bottom: 0; 86 - padding-left: 0; 87 - padding-right: 0; 88 - padding-top: 0; 89 - margin-bottom: 1.75rem; 90 - list-style-image: none; 91 - } 92 - 93 - .markdown li:not(.unstyled) { 94 - margin-bottom: calc(1.75rem / 2); 95 - } 96 - 97 - .markdown img { 98 - @apply mb-8; 99 - max-width: 100%; 3 + line-height: 28px; 4 + --path: none; 5 + --radius-top: 12px; 6 + --radius-bottom: 12px; 7 + --padding-top: 1rem; 8 + --padding-bottom: 1rem; 100 9 } 101 10 102 11 .markdown iframe { 103 - @apply mb-8; 104 - max-width: 100%; 105 - } 106 - 107 - .markdown ol { 108 - @apply mb-8 list-inside md:list-outside list-decimal; 12 + width: calc(100% + 2rem) !important; 13 + margin-left: -1rem; 109 14 } 110 15 111 - .markdown input { 112 - color: #222; 16 + @media (prefers-color-scheme: dark) { 17 + .markdown iframe { 18 + border-color: #444 !important; 19 + } 113 20 } 114 21 115 - .markdown table { 116 - width: 100%; 117 - border-collapse: collapse; 118 - margin-bottom: 1.5rem; 22 + @media (max-width: 672px) { 23 + .markdown pre, 24 + .markdown iframe { 25 + width: calc(100% + 2.5rem) !important; 26 + margin-left: -1.25rem; 27 + margin-right: -1.25rem; 28 + } 119 29 } 120 30 121 - .markdown th, 122 - .markdown td { 123 - border: 1px solid #dcdcdc; 124 - padding: 8px; 125 - text-align: left; 31 + @media (max-width: 550px) { 32 + .markdown pre { 33 + border-radius: 0 !important; 34 + } 35 + .markdown iframe { 36 + border-left: none !important; 37 + border-right: none !important; 38 + } 126 39 } 127 40 128 - @media (prefers-color-scheme: dark) { 129 - .markdown input { 130 - color: #111; 131 - } 132 - } 133 - 41 + /* Code line highlighting - data-attribute from rehype-pretty-code */ 134 42 .markdown pre [data-highlighted-line] { 135 - margin-left: -16px; 136 - margin-right: -16px; 137 - padding-left: 12px; 138 - border-left: 4px solid #ffa7c4; 139 - background-color: #022a4b; 140 - display: block; 141 - padding-right: 1em; 43 + margin-left: -16px; 44 + margin-right: -16px; 45 + padding-left: 12px; 46 + border-left: 4px solid #ffa7c4; 47 + background-color: #022a4b; 48 + display: block; 49 + padding-right: 1em; 142 50 } 143 51 52 + /* Tip button styles */ 144 53 .tip { 145 - @apply inline-block px-8 py-4 font-sans font-semibold text-xl rounded-full shadow-xl transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-opacity-50 border-b-[1px] text-white overflow-clip transition-transform; 146 - border-color: var(--link); 54 + @apply inline-block px-8 py-4 font-sans font-semibold text-xl rounded-full shadow-xl transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-opacity-50 border-b-[1px] text-white overflow-clip transition-transform; 55 + border-color: var(--link); 147 56 } 148 57 149 58 .tip.tip-sm { 150 - @apply inline-block px-6 py-2 text-lg shadow-md; 59 + @apply inline-block px-6 py-2 text-lg shadow-md; 151 60 } 152 61 153 62 .tip-bg { 154 - background-image: linear-gradient(45deg, var(--purple), var(--pink)); 155 - position: absolute; 156 - top: 0; 157 - left: 0; 158 - right: 0; 159 - bottom: 0; 160 - z-index: -1; 63 + background-image: linear-gradient(45deg, var(--purple), var(--pink)); 64 + position: absolute; 65 + top: 0; 66 + left: 0; 67 + right: 0; 68 + bottom: 0; 69 + z-index: -1; 161 70 } 162 71 163 72 @media (prefers-color-scheme: dark) { 164 - .tip-bg { 165 - filter: brightness(0.46); 166 - } 73 + .tip-bg { 74 + filter: brightness(0.46); 75 + } 167 76 }
+166
app/[slug]/markdown.tsx
··· 1 + "use client"; 2 + 3 + export function P(props: React.ComponentProps<"p">) { 4 + return <p {...props} />; 5 + } 6 + 7 + export function H2({ id, children, ...props }: React.ComponentProps<"h2">) { 8 + return ( 9 + <h2 10 + id={id} 11 + className="group relative text-3xl font-bold mt-2" 12 + {...props} 13 + > 14 + <a href={`#${id}`} className="no-underline text-inherit"> 15 + <span 16 + aria-hidden 17 + className="absolute -translate-x-[1em] opacity-0 group-hover:opacity-70 group-focus-within:opacity-70 group-[:target]:opacity-70" 18 + > 19 + # 20 + </span> 21 + {children} 22 + </a> 23 + </h2> 24 + ); 25 + } 26 + 27 + export function H3({ id, children, ...props }: React.ComponentProps<"h3">) { 28 + return ( 29 + <h3 30 + id={id} 31 + className="group relative text-2xl font-bold mt-2" 32 + {...props} 33 + > 34 + <a href={`#${id}`} className="no-underline text-inherit"> 35 + <span 36 + aria-hidden 37 + className="absolute -translate-x-[1em] opacity-0 group-hover:opacity-70 group-focus-within:opacity-70 group-[:target]:opacity-70" 38 + > 39 + # 40 + </span> 41 + {children} 42 + </a> 43 + </h3> 44 + ); 45 + } 46 + 47 + export function H4({ id, children, ...props }: React.ComponentProps<"h4">) { 48 + return ( 49 + <h4 50 + id={id} 51 + className="group relative text-xl font-bold mt-2" 52 + {...props} 53 + > 54 + <a href={`#${id}`} className="no-underline text-inherit"> 55 + <span 56 + aria-hidden 57 + className="absolute -translate-x-[1em] opacity-0 group-hover:opacity-70 group-focus-within:opacity-70 group-[:target]:opacity-70" 58 + > 59 + # 60 + </span> 61 + {children} 62 + </a> 63 + </h4> 64 + ); 65 + } 66 + 67 + export function Blockquote(props: React.ComponentProps<"blockquote">) { 68 + return ( 69 + <blockquote 70 + className="relative -left-2 -ml-4 pl-4 italic opacity-80 border-l-[3px] border-current" 71 + {...props} 72 + /> 73 + ); 74 + } 75 + 76 + export function UL(props: React.ComponentProps<"ul">) { 77 + return <ul className="list-inside md:list-outside list-disc" {...props} />; 78 + } 79 + 80 + export function OL(props: React.ComponentProps<"ol">) { 81 + return ( 82 + <ol className="list-inside md:list-outside list-decimal" {...props} /> 83 + ); 84 + } 85 + 86 + export function LI(props: React.ComponentProps<"li">) { 87 + return <li className="mb-3 last:mb-0" {...props} />; 88 + } 89 + 90 + export function Pre({ 91 + style, 92 + ...props 93 + }: React.ComponentProps<"pre">) { 94 + return ( 95 + <pre 96 + className="-mx-4 overflow-y-auto p-4 text-sm" 97 + {...props} 98 + style={{ 99 + ...style, 100 + clipPath: "var(--path, none)", 101 + borderTopLeftRadius: "var(--radius-top, 12px)", 102 + borderTopRightRadius: "var(--radius-top, 12px)", 103 + borderBottomLeftRadius: "var(--radius-bottom, 12px)", 104 + borderBottomRightRadius: "var(--radius-bottom, 12px)", 105 + paddingTop: "var(--padding-top, 1rem)", 106 + paddingBottom: "var(--padding-bottom, 1rem)", 107 + }} 108 + /> 109 + ); 110 + } 111 + 112 + export function Code({ 113 + className, 114 + ...props 115 + }: React.ComponentProps<"code"> & { "data-language"?: string }) { 116 + // Code blocks have data-language from rehype-pretty-code (defaultLang ensures all blocks have it) 117 + if ("data-language" in props) { 118 + return <code className={className} {...props} />; 119 + } 120 + // Inline code styling 121 + return ( 122 + <code 123 + className="rounded-[10px] bg-[--inlineCode-bg] text-[--inlineCode-text] px-[0.2em] py-[0.15em] whitespace-normal" 124 + {...props} 125 + /> 126 + ); 127 + } 128 + 129 + export function Table(props: React.ComponentProps<"table">) { 130 + return <table className="w-full border-collapse" {...props} />; 131 + } 132 + 133 + export function Th(props: React.ComponentProps<"th">) { 134 + return ( 135 + <th 136 + className="border border-gray-300 dark:border-gray-600 p-2 text-left" 137 + {...props} 138 + /> 139 + ); 140 + } 141 + 142 + export function Td(props: React.ComponentProps<"td">) { 143 + return ( 144 + <td 145 + className="border border-gray-300 dark:border-gray-600 p-2 text-left" 146 + {...props} 147 + /> 148 + ); 149 + } 150 + 151 + export function Hr(props: React.ComponentProps<"hr">) { 152 + return <hr className="opacity-60 dark:opacity-10 mt-4" {...props} />; 153 + } 154 + 155 + export function Img(props: React.ComponentProps<"img">) { 156 + return <img className="max-w-full" {...props} />; 157 + } 158 + 159 + export function A(props: React.ComponentProps<"a">) { 160 + return ( 161 + <a 162 + className="border-b border-[--link] text-[--link]" 163 + {...props} 164 + /> 165 + ); 166 + }
+51 -49
app/[slug]/page.tsx
··· 2 2 import { readdir, readFile } from "fs/promises"; 3 3 import matter from "gray-matter"; 4 4 import { MDXRemote } from "next-mdx-remote-client/rsc"; 5 - import Link from "../Link"; 5 + import TextLink from "../TextLink"; 6 6 import { sans } from "../fonts"; 7 7 import remarkSmartpants from "remark-smartypants"; 8 8 import rehypePrettyCode from "rehype-pretty-code"; 9 9 import rehypeSlug from "rehype-slug"; 10 - import rehypeAutolinkHeadings from "rehype-autolink-headings"; 11 10 import { remarkMdxEvalCodeBlock } from "./mdx"; 12 11 import overnight from "overnight/themes/Overnight-Slumber.json"; 13 12 import "./markdown.css"; 14 13 import remarkGfm from "remark-gfm"; 14 + import * as markdown from "./markdown"; 15 15 16 16 overnight.colors["editor.background"] = "var(--code-bg)"; 17 17 ··· 54 54 year: "numeric", 55 55 })} 56 56 </p> 57 - <div className="markdown"> 58 - <div className="mb-8 relative md:-left-6 flex flex-wrap items-baseline"> 59 - {!data.nocta && ( 60 - <a 61 - href="https://ko-fi.com/gaearon" 62 - target="_blank" 63 - className="mt-10 tip tip-sm mr-4" 64 - > 65 - <span className="tip-bg" /> 66 - Pay what you like 67 - </a> 68 - )} 69 - {data.youtube && ( 70 - <a 71 - className="leading-tight mt-4" 72 - href={data.youtube} 73 - target="_blank" 74 - > 75 - <span className="hidden min-[400px]:inline">Watch on </span> 76 - YouTube 77 - </a> 78 - )} 79 - </div> 57 + <div className="markdown flex flex-col gap-8 mt-12"> 58 + {(!data.nocta || data.youtube) && ( 59 + <div className="relative md:-left-6 flex flex-wrap items-baseline gap-4"> 60 + {!data.nocta && ( 61 + <a 62 + href="https://ko-fi.com/gaearon" 63 + target="_blank" 64 + className="tip tip-sm" 65 + > 66 + <span className="tip-bg" /> 67 + Pay what you like 68 + </a> 69 + )} 70 + {data.youtube && ( 71 + <TextLink href={data.youtube}> 72 + <span className="hidden min-[400px]:inline">Watch on </span> 73 + YouTube 74 + </TextLink> 75 + )} 76 + </div> 77 + )} 80 78 81 79 <Wrapper> 80 + <div className="flex flex-col gap-8"> 82 81 <MDXRemote 83 82 source={content} 84 83 components={{ 85 - a: Link, 84 + p: markdown.P, 85 + h2: markdown.H2, 86 + h3: markdown.H3, 87 + h4: markdown.H4, 88 + blockquote: markdown.Blockquote, 89 + ul: markdown.UL, 90 + ol: markdown.OL, 91 + li: markdown.LI, 92 + pre: markdown.Pre, 93 + code: markdown.Code, 94 + table: markdown.Table, 95 + th: markdown.Th, 96 + td: markdown.Td, 97 + hr: markdown.Hr, 98 + a: (props: React.ComponentProps<"a">) => ( 99 + <TextLink {...props} href={props.href ?? ""} /> 100 + ), 86 101 img: async ({ src, ...rest }) => { 87 102 if ( 88 103 src && ··· 121 136 finalSrc = `/${slug}/${src}`; 122 137 } 123 138 124 - return <img src={finalSrc} {...rest} />; 139 + return <markdown.Img src={finalSrc} {...rest} />; 125 140 }, 126 141 Video: ({ src, ...rest }) => { 127 142 let finalSrc = src; ··· 146 161 rehypePrettyCode, 147 162 { 148 163 theme: overnight, 164 + defaultLang: { block: "text" }, 149 165 }, 150 166 ], 151 167 [rehypeSlug], 152 - [ 153 - rehypeAutolinkHeadings, 154 - { 155 - behavior: "wrap", 156 - properties: { 157 - className: "linked-heading", 158 - target: "_self", 159 - }, 160 - }, 161 - ], 162 168 ] as any, 163 169 } as any, 164 170 }} 165 171 /> 172 + </div> 166 173 </Wrapper> 167 174 {!data.nocta && ( 168 - <div className="flex flex-wrap items-baseline"> 175 + <div className="flex flex-wrap items-baseline gap-4 relative md:-left-8"> 169 176 <a 170 177 href="https://ko-fi.com/gaearon" 171 178 target="_blank" 172 - className="tip mb-8 relative md:-left-8" 179 + className="tip" 173 180 > 174 181 <span className="tip-bg" /> 175 182 Pay what you like 176 183 </a> 177 - <a 178 - className="leading-tight ml-4 relative md:-left-8" 179 - href="/im-doing-a-little-consulting/" 180 - > 181 - Hire me 182 - </a> 184 + <TextLink href="/hire-me-in-japan/">Hire me</TextLink> 183 185 </div> 184 186 )} 185 - <hr /> 187 + <hr className="opacity-60 dark:opacity-10" /> 186 188 <p> 187 189 {data.bluesky && ( 188 190 <> 189 - <Link href={data.bluesky}>Discuss on Bluesky</Link> 191 + <TextLink href={data.bluesky}>Discuss on Bluesky</TextLink> 190 192 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 191 193 </> 192 194 )} 193 195 {data.youtube && ( 194 196 <> 195 - <Link href={data.youtube}>Watch on YouTube</Link> 197 + <TextLink href={data.youtube}>Watch on YouTube</TextLink> 196 198 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 197 199 </> 198 200 )} 199 201 {/* TODO: This should say Edit when Tangled adds an editor. */} 200 - <Link href={editUrl}>Fork on Tangled</Link> 202 + <TextLink href={editUrl}>Fork on Tangled</TextLink> 201 203 </p> 202 204 </div> 203 205 </article>
+6 -6
public/a-complete-guide-to-useeffect/index.md
··· 89 89 } 90 90 ``` 91 91 92 - What does it mean? Does `count` somehow “watch” changes to our state and update automatically? That might be a useful first intuition when you learn React but it’s *not* an [accurate mental model](https://overreacted.io/react-as-a-ui-runtime/). 92 + What does it mean? Does `count` somehow “watch” changes to our state and update automatically? That might be a useful first intuition when you learn React but it’s *not* an [accurate mental model](/react-as-a-ui-runtime/). 93 93 94 94 **In this example, `count` is just a number.** It’s not a magic “data binding”, a “watcher”, a “proxy”, or anything else. It’s a good old number like this one: 95 95 ··· 140 140 141 141 The key takeaway is that the `count` constant inside any particular render doesn’t change over time. It’s our component that’s called again — and each render “sees” its own `count` value that’s isolated between renders. 142 142 143 - *(For an in-depth overview of this process, check out my post [React as a UI Runtime](https://overreacted.io/react-as-a-ui-runtime/).)* 143 + *(For an in-depth overview of this process, check out my post [React as a UI Runtime](/react-as-a-ui-runtime/).)* 144 144 145 145 ## Each Render Has Its Own Event Handlers 146 146 ··· 190 190 191 191 Go ahead and [try it yourself!](https://codesandbox.io/s/w2wxl3yo0l) 192 192 193 - If the behavior doesn’t quite make sense to you, imagine a more practical example: a chat app with the current recipient ID in the state, and a Send button. [This article](https://overreacted.io/how-are-function-components-different-from-classes/) explores the reasons in depth but the correct answer is 3. 193 + If the behavior doesn’t quite make sense to you, imagine a more practical example: a chat app with the current recipient ID in the state, and a Send button. [This article](/how-are-function-components-different-from-classes/) explores the reasons in depth but the correct answer is 3. 194 194 195 195 The alert will “capture” the state at the time I clicked the button. 196 196 ··· 394 394 395 395 **Conceptually, you can imagine effects are a *part of the render result*.** 396 396 397 - Strictly saying, they’re not (in order to [allow Hook composition](https://overreacted.io/why-do-hooks-rely-on-call-order/) without clumsy syntax or runtime overhead). But in the mental model we’re building up, effect functions *belong* to a particular render in the same way that event handlers do. 397 + Strictly saying, they’re not (in order to [allow Hook composition](/why-do-hooks-rely-on-call-order/) without clumsy syntax or runtime overhead). But in the mental model we’re building up, effect functions *belong* to a particular render in the same way that event handlers do. 398 398 399 399 --- 400 400 ··· 518 518 519 519 **It doesn’t matter whether you read from props or state “early” inside of your component.** They’re not going to change! Inside the scope of a single render, props and state stay the same. (Destructuring props makes this more obvious.) 520 520 521 - Of course, sometimes you *want* to read the latest rather than captured value inside some callback defined in an effect. The easiest way to do it is by using refs, as described in the last section of [this article](https://overreacted.io/how-are-function-components-different-from-classes/). 521 + Of course, sometimes you *want* to read the latest rather than captured value inside some callback defined in an effect. The easiest way to do it is by using refs, as described in the last section of [this article](/how-are-function-components-different-from-classes/). 522 522 523 523 Be aware that when you want to read the *future* props or state from a function in a *past* render, you’re swimming against the tide. It’s not *wrong* (and in some cases necessary) but it might look less “clean” to break out of the paradigm. This is an intentional consequence because it helps highlight which code is fragile and depends on timing. In classes, it’s less obvious when this happens. 524 524 ··· 628 628 629 629 ## Synchronization, Not Lifecycle 630 630 631 - One of my favorite things about React is that it unifies describing the initial render result and the updates. This [reduces the entropy](https://overreacted.io/the-bug-o-notation/) of your program. 631 + One of my favorite things about React is that it unifies describing the initial render result and the updates. This [reduces the entropy](/the-bug-o-notation/) of your program. 632 632 633 633 Say my component looks like this: 634 634
+123
public/hire-me-in-japan/index.md
··· 1 + --- 2 + title: Hire Me in Japan 3 + date: '2025-11-11' 4 + spoiler: I'm looking for a new job. 5 + nocta: true 6 + --- 7 + 8 + My sabbatical is soon coming to an end, and I am looking for a new job. 9 + 10 + In particular, I am looking for a job at a company that would like to **sponsor a working visa for me in Japan**, where I'd like to relocate within the next year. 11 + 12 + If you can sponsor a software engineering visa in Japan and think I might be a good fit, please email `dan.abramov.japan@gmail.com`. Below I'll recap some of my past work, with more details re: what I'm looking for near the end of this page. 13 + 14 + [*(Skip to the end)*](#looking-forward) 15 + 16 + --- 17 + 18 + ### Past Work 19 + 20 + Hi! My name is Dan Abramov. 21 + 22 + I started programming about 20 years ago and then I couldn't stop--so I've been doing software for over 15 years professionally. I've dabbled in different languages, but the vast majority of my recent work has been in JavaScript and TypeScript. 23 + 24 + Here's a few things I have worked on over the years. 25 + 26 + #### 2025: Consulting 27 + 28 + This year, [I've been doing consulting](/im-doing-a-little-consulting/), so there isn't much I can say publicly. 29 + 30 + Mostly, I've been consulting teams using React on how to approach engineering challenges related to performance and state management in complex apps. 31 + 32 + On the side, I've been [contributing](https://github.com/teorth/analysis/pulls?q=is%3Apr+author%3Agaearon+is%3Aclosed) to Terence Tao's [Analysis textbook in Lean](https://github.com/teorth/analysis). 33 + 34 + #### 2023–2025: The Bluesky app 35 + 36 + From 2023 to 2025, I worked on the official [Bluesky](https://bsky.app/) client app (in React Native). Most of my engineering work focused on [improving the app quality](https://github.com/bluesky-social/social-app/issues/1675), for example: 37 + 38 + - [Implementing animations for the lightbox](https://github.com/bluesky-social/social-app/pull/6048) 39 + - [Synchronizing feed swipe animations with gestures](https://github.com/bluesky-social/social-app/pull/6868) 40 + - [Introducing custom lint rules to prevent runtime crashes](https://github.com/bluesky-social/social-app/pull/3398) 41 + - [Shaving off seconds from Android start time](https://github.com/bluesky-social/social-app/pull/1756) 42 + 43 + Some of this work required multi-week deep dives and sprawling refactors. 44 + 45 + - [Rewriting the lightbox to work well on Android](https://github.com/bluesky-social/social-app/pull/1624) 46 + - [Reworking the cross-platform layout to work well on the web](https://github.com/bluesky-social/social-app/pull/2126) 47 + - [Revamping how threads show up in the feed](https://github.com/bluesky-social/social-app/pull/4871) 48 + - [Revamping the post composer into a thread composer](https://github.com/bluesky-social/social-app/pull/5962) 49 + 50 + The Bluesky app is open source; here's a link to my [other merged pull requests](https://github.com/bluesky-social/social-app/pulls?q=is%3Apr+author%3Agaearon+is%3Amerged+). 51 + 52 + We've used a lot of open source software at Bluesky, and sometimes it could be difficult to trace down where a bug is coming from. Occasionally, I've had to do deep dive investigations into the underlying projects to file bugs or fixes there. 53 + 54 + - [Reducing the Reanimated serialization traffic](https://github.com/bluesky-social/social-app/pull/6219) 55 + - [Fixing a 10 year old ScrollView bug in React Native](https://github.com/facebook/react-native/pull/47591) 56 + - [Documenting the difficulties in getting gestures to work with ScrollView](https://github.com/software-mansion/react-native-gesture-handler/issues/2616#issuecomment-2525238174) 57 + 58 + In addition to my individual engineering work, I've done some engineering management and mentoring on the application team. This included: 59 + 60 + - Helping the team get a natural mental model for working with React. 61 + - Mentoring the application team on how to root cause particularly gnarly issues. 62 + - Pushing for a closer collaboration with the backend team so that the seams from the client/server organizational split don't "show up" as poor UX in the product. 63 + 64 + I've also helped the team explain the AT protocol to a broader community--first in a [talk](https://www.youtube.com/watch?v=F1sJW6nTP6E) last year, which then crystallized into my recent article called [Open Social](/open-social/). 65 + 66 + #### 2015–2023: React at Meta/Facebook 67 + 68 + Before Bluesky, I used to work on the React team at Meta (formerly Facebook). Some of the more visible projects I was involved with include: 69 + 70 + - The [React Documentation](https://react.dev/), which I co-wrote with [Rachel Lee Nabors](https://nearestnabors.com/) and [others](https://react.dev/community/docs-contributors). My work on it included designing the Learn section curriculum, iterating on the Reference page structure, a decent chunk of technical work on the website itself, and much of the actual writing, including the design of examples and challenges. 71 + - The public messaging and rollout of React Hooks, including the original documentation for React Hooks and the conference talk introducing them. 72 + - Implementing Fast Refresh (hot reloading / live editing) as a first-class React feature. It feels like a distant past, but we used to press Cmd+R to see our edits. 73 + - Co-creating [Create React App](https://legacy.reactjs.org/blog/2016/07/22/create-apps-with-no-configuration.html), which ended the "JavaScript fatigue" (allegedly). 74 + 75 + I've also done [some other technical work on React over the years](https://github.com/facebook/react/pulls?q=is%3Apr+author%3Agaearon+is%3Amerged+), which mostly consisted of fixing bugs and [occasionally](https://legacy.reactjs.org/blog/2017/12/15/improving-the-repository-infrastructure.html) revamping the build infrastructure. 76 + 77 + Outside of my job, I co-created [Just JavaScript](https://justjavascript.com/), which is a half-book half-course introduction to thinking in JavaScript, aiming to be both whimsical *and* rigorous. 78 + 79 + #### Before 2015 80 + 81 + I haven't done many publicly notable things before getting hired at Facebook, other than accidentally co-creating [Redux](https://redux.js.org/) as a demo for my conference talk. 82 + 83 + I've previously worked at a small product company that didn't succeed doing some JavaScript and C#, and before that at an outsourcing firm where I mostly did C#. 84 + 85 + I also created [React Hot Loader](https://gaearon.github.io/react-hot-loader/) (a very early precursor to Fast Refresh, now built into React), [React DnD](https://react-dnd.github.io/react-dnd/), and [normalizr](https://github.com/paularmstrong/normalizr) (which was used by Twitter for some time). 86 + 87 + --- 88 + 89 + ### Looking Forward 90 + 91 + As you can tell from the above, most of my engineering expertise lies in the field of UI engineering, web development in particular, and of course in using React. I'd love an opportunity to share my expertise and to help you create better apps. 92 + 93 + I enjoy sharing what I learned, which is why I blog and do [talks](https://danabra.mov/). Although I prefer when I can work on open source software, this will not be a requirement for me. 94 + 95 + Currently I feel most comfortable with JavaScript/TypeScript. I've [recently](/the-math-is-haunted/) [picked](/beyond-booleans/) [up](/a-lean-syntax-primer/) some Lean, and working with it professionally seems alluring (if unlikely). I've been using LLMs quite a bit recently too and am open to working with them more. In general, I'm open to learning new things on the job (for example, I've learned React Native while at Bluesky!) but a new stack may require some ramp-up time. 96 + 97 + I care about the *quality* of what I'm working on, whether it's an app, a developer tool, or a course. I hope to find a job where caring about quality is appreciated. I know fixing things "the right way" isn't always possible, but I feel most useful when I'm able to dig into the root causes, and work with others on solving them. 98 + 99 + To sum up, I'm looking for a software engineering job... in Japan. 100 + 101 + --- 102 + 103 + ### Why Japan 104 + 105 + Why indeed! 106 + 107 + The honest answer is that the feeling of home has moved, and so now I must try to move along with it. I don't know how it happened exactly. My wife and I first visited Japan a year ago, staying for a few months in Kyoto. After coming home to London, we realized that something was off, so this spring we came back to Kyoto for a few more months. And now we're back in Kyoto for a few more months again. (Big thanks to the [Vue Fes Japan 2025](https://vuefes.jp/2025/) organizers for covering our recent flights!) 108 + 109 + Paying rent in both cities is expensive, flights are disruptive, and we want to adopt a cat, so we have to choose. We've loved spending a whole decade in London, but this decade, Kyoto feels right. The question isn't whether to move but--*can it be?* 110 + 111 + I haven't learned Japanese yet, which significantly limits my options. I've learned hiragana and katakana during the last trip, so now I'm focused on N5 grammar and vocab. I pick up my speed a bit slowly so I expect Japanese to take me a while. 112 + 113 + Since I don't speak Japanese yet *and* I'm hoping to relocate to Kyoto, I suspect that the most promising option to obtain a work visa will either be an international company with a Japanese presence, or a Japanese company that permits remote work (I assume there aren't many tech opportunities in Kyoto itself!) and that does most of the work-related communication in English. (I do hope to get to a fluent level of Japanese eventually, but that will likely take me at least a few more years.) 114 + 115 + I know there aren't many options like this, but fingers crossed. I'm talking to a few companies but I would like to get a better sense of the options--hence this post. My hope is to find a real job that lets me come back with a working visa (rather than as a tourist, like now) and to settle down in Japan for the foreseeable future. 116 + 117 + **If you would like to explore working with me and can sponsor my work visa in Japan, please reach out via `dan.abramov.japan@gmail.com` and let me know!** 118 + 119 + And if you know someone who might, any leads and intros are very appreciated. 120 + 121 + Thank you. 122 + 123 + Dan
+1 -1
public/how-imports-work-in-rsc/index.md
··· 196 196 197 197 In that sense, when you `import` some code, you bring it *into* your program. 198 198 199 - But what if we want to write *both our backend and frontend* in JavaScript? (Or, alternatively, what if we realize that adding a [JS BFF can make our app better?](https://overreacted.io/jsx-over-the-wire/#backend-for-frontend)) 199 + But what if we want to write *both our backend and frontend* in JavaScript? (Or, alternatively, what if we realize that adding a [JS BFF can make our app better?](/jsx-over-the-wire/#backend-for-frontend)) 200 200 201 201 --- 202 202
+242
public/how-to-fix-any-bug/index.md
··· 1 + --- 2 + title: How to Fix Any Bug 3 + date: '2025-10-21' 4 + spoiler: The joys of vibecoding. 5 + bluesky: https://bsky.app/profile/danabra.mov/post/3m3o3jjzafk2k 6 + --- 7 + 8 + I've been vibecoding a little app, and a few days ago I ran into a bug. 9 + 10 + The bug went something like this. Imagine a route in a webapp. That route shows a sequence of steps--essentially, cards. Each card has a button that scrolls down to the next card. Everything works great. However, as soon as I tried to *also* call the server from that button, scrolling would no longer work. It would jitter and break. 11 + 12 + So, adding a remote call somehow broke scrolling. 13 + 14 + I wasn't sure what's causing the bug. Clearly, the newly added remote server call (which I was doing via [React Router actions](https://reactrouter.com/start/framework/actions)) was somehow interfering with my [`scrollIntoView`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) call. But I couldn't tell how. I initially thought the problem was that React Router re-renders my page (an action causes a data refetch), but in principle there's no reason why a refetch would interfere with an ongoing scroll. The server was returning the same items, so it shouldn't have changed anything. 15 + 16 + In React, a re-render [should](/writing-resilient-components/#principle-2-always-be-ready-to-render) always be safe to do. Something else was wrong--either in my code, or in React Router, or in React, or even in the browser itself. 17 + 18 + How do I fix this bug? 19 + 20 + *Can I get Claude to fix it?* 21 + 22 + --- 23 + 24 + ## Step 0: Just Fix It 25 + 26 + I told Claude to fix the problem. 27 + 28 + Claude tried a few things. It rewrote conditions in the `useEffect` that contains the `scrollIntoView` call and said that the bug was fixed. But that didn't help. It then tried changing `smooth` scrolling to `instant`, and a few other things. 29 + 30 + Each time, Claude would proudly declare that the problem was solved. 31 + 32 + But it was not! 33 + 34 + The bug was still there. 35 + 36 + This might sound like I'm complaining about Claude, but really the impetus for writing this article is that I see human engineers (including myself) make the same mistakes. So I wanted to document the process I usually follow to fix bugs. 37 + 38 + Why was Claude repeatedly wrong? 39 + 40 + Claude was repeatedly wrong because **it didn't have a repro**. 41 + 42 + --- 43 + 44 + ## Step 1: Find a Repro 45 + 46 + A *repro*, or a reproducing case, is a sequence of instructions then, when followed, gives you a reliable way to tell whether the bug is still happening. It's "the test". A repro says what to do, what's *expected* to happen, and what is *actually* happening. 47 + 48 + From my perspective, I already had a good repro: 49 + 50 + 1. Click the button. 51 + 2. The *expected* behavior was scrolling down, but the *actual* behavior was scroll jitter. 52 + 53 + Even better, the bug happened every time. 54 + 55 + If my repro was unreliable (e.g. if it happened just 30% of attempts), I'd either have to gradually remove different sources of uncertainty (e.g. recording the network and mocking it in future attempts), or live with the producitivity hit of having to test every potential fix many more times. But luckily, my repro was reliable. 56 + 57 + And yet, to Claude, my repro essentially didn't exist. 58 + 59 + The problem is that "scrolling jitters" from my repro didn't mean anything to Claude. Claude doesn't have eyes or other ways to perceive the jitter directly. So Claude was essentially operating *without* a repro--it tried to fix the bug, but didn't do anything specific to verify it. That is too common, even with the best of us. 60 + 61 + In this case, Claude *couldn't* have followed my repro exactly since it couldn't "look" at the screen (taking a few screenshots wouldn't capture it). So my first repro was unsuitable if I wanted Claude to fix it. This might seem like a problem with Claude, but it's actually not uncommon when working with other people--sometimes a bug only happens on one machine, or for a specific user, or with specific settings. 62 + 63 + Luckily, there is a trick. You can trade a repro for *another* repro as long as you're able to convince yourself that it'll help you make progress on the original problem. 64 + 65 + Here's how you can change your repro, and some things to watch out for. 66 + 67 + --- 68 + 69 + ## Step 2: Narrow the Repro 70 + 71 + Changing the repro you're working with is always a risk. The risk is that the new repro has nothing to do with your original bug, and solving it is a waste of time. 72 + 73 + However, sometimes changing a repro is unavoidable (Claude can't look at my screen, so I have to come up with something else). And sometimes it is hugely beneficial for iteration (say, a repro that takes ten seconds is vastly more valuable than a repro that takes ten minutes). So learning to change repros is important. 74 + 75 + Ideally, you'd trade a repro for a simpler, narrower, more direct repro. 76 + 77 + Here's the idea I suggested to Claude: 78 + 79 + 1. Measure the document scroll position. 80 + 2. Click the button. 81 + 3. Measure the document scroll position again. 82 + 4. The *expected* behavior is that there is a delta, the *actual* behavior is there's none. 83 + 84 + My thinking was that this seems roughly equivalent to the problem I saw with my own eyes. Although this repro doesn't capture the jitter, failing to scroll down is likely related. Even if it's not the *only* problem, it's worth fixing this on its own. 85 + 86 + Claude added some `console.log`s, opened the page via Playwright MCP, and clicked around. Indeed, the scroll position was *not* changing despite button click. 87 + 88 + Okay, so now Claude *is* able to verify the bug exists! 89 + 90 + Are we done with finding the repro? 91 + 92 + Actually, we're not! 93 + 94 + One common pitfall with narrowing a repro is that you *think* you found a good one, but actually your new repro captures some unrelated problem that presents in a similar way. **This is a very expensive mistake to make** because you can waste hours chasing solutions to a different problem than the one you wanted to solve. 95 + 96 + For example, it's possible that Claude simply was reading the scroll position *too early*, and even if the bug *was* fixed, it would still "see" the position unchanging. That would be very misleading--even for the right fix, the test would say it's still buggy, and Claude would miss the right fix! That happens to human engineers too. 97 + 98 + **This is why, whenever you narrow a repro, you should also confirm that a *positive* result ("everything works") is still _possible_ to obtain with the new repro.** 99 + 100 + This is easier to explain by an example. 101 + 102 + I told Claude to comment out the network call (which originally surfaced the bug). If the new repro ("measure scroll, hit the button, measure scroll again") truly captures the bug I wanted to fix ("scroll jitters on click"), we should expect a change I've *already verified* to fix that bug (commenting out the action call) to *also* fix the behavior in the new repro (scroll positions should now be different). 103 + 104 + And that's what happened! Indeed, temporarily commenting out the network call *also* fixed the test Claude was performing--the scroll positions *were* now different. 105 + 106 + At this point, it's worth trying to change the code a few times in either direction (comment it in, comment it out) to verify that each edit predicts the new repro result. (It's also worth doing other edits to rule out that every second edit works.) 107 + 108 + The scrolling measurement still seemed correlated to that network call. 109 + 110 + This still doesn't mean that the new repro represents the same problem. It could be something unrelated, or just a part of it. **It's always a risk to change the repro.** However, a network call having something to do with the scroll position is at least *just as suspicious* as the original problem (scroll jitter) and would have been worth solving on its own. This is a good enough reason to continue with the new repro. 111 + 112 + --- 113 + 114 + ## Step 3: Remove Everything Else 115 + 116 + I created a new branch and asked Claude to follow the following workflow: 117 + 118 + 1. Run the repro to verify the bug is present. 119 + 2. Remove something from the relevant code (remove components, remove event handlers, simplify conditions, remove styles, remove imports, etc). 120 + 3. Run the repro again to verify if the bug is still present. 121 + 4. If the bug is still there, commit the changes. 122 + 5. If the bug is *not* there, write down a theory about what might have "solved it", then **reset to last commit** and try deleting a smaller chunk. 123 + 124 + I was about to step out so I told Claude to keep at it and to not rest until it's narrowed down the repro to something that can't be further reduced--a React component with no extra libraries (not even React Router) and minimal logic. 125 + 126 + When I came back, Claude created a few reproducing cases for me, but frustratingly, none of them were exhibiting the bug: 127 + ``` 128 + ⏺ I've done extensive investigation and created three progressively more 129 + accurate minimal reproductions. All of them work correctly, but the real 130 + page fails. I've tested: 131 + 132 + - Plain React state updates 133 + - React Router revalidation 134 + - Component remounting 135 + - New object references 136 + - Synchronous loaders 137 + - Various dependency arrays 138 + - Multiple renders during scroll 139 + 140 + Everything I can think of has been tested and ruled out. The bug 141 + consistently reproduces in the real app but not in any repro. This 142 + suggests there's something very specific about the page 143 + environment that I haven't captured. 144 + 145 + The workaround (deferring network call with setTimeout) works reliably, 146 + which confirms that the issue is the timing between the 147 + mutation/revalidation and the scroll. But I cannot determine WHY React 148 + Router's revalidation breaks scrollIntoView in the real app when it 149 + doesn't in the repros. 150 + 151 + I've committed all the repros and investigation notes for future 152 + reference. 153 + ``` 154 + 155 + Does this mean narrowing down the bug doesn't always work? 156 + 157 + No. 158 + 159 + It means Claude failed to follow my instructions. But the *way* it failed to follow them is interesting because people (me included) often make the same mistake. 160 + 161 + While Claude was simplifying the code, it started forming *theories*. Maybe this effect is buggy. Maybe there's something to do with remounting. Maybe React is doing something weird. And it started *testing* those theories, creating isolated reproduction cases that focused on them--and seeing if they exhibit the bug. 162 + 163 + Creating theories and testing them is great! We should definitely do that. 164 + 165 + But have a look at my instructions again: 166 + 167 + 1. Run the repro to verify the bug is present. 168 + 2. Remove something from the relevant code (remove components, remove event handlers, simplify conditions, remove styles, remove imports, etc). 169 + 3. Run the repro again to verify if the bug is still present. 170 + 4. If the bug is still there, commit the changes. 171 + 5. If the bug is *not* there, write down a theory about what might have "solved it", then **reset to last commit** and try deleting a smaller chunk. 172 + 173 + There's something specific I was trying to get it to do. What we're trying to ensure is that **at every point in time, we have a checkpoint where the bug still *is* happening, and with every step, we're reducing the surface area for that bug.** 174 + 175 + Claude got too carried away testing its own theories and ended up with a bunch of test cases that don't actually exhibit the bug. Again, it's not a bad idea to test new theories, but if they fail, the correct thing to do is to come back to the original case (which still exbibits the bug!) and to keep removing things until we find the cause. 176 + 177 + This reminds me of the concept of *well-founded recursion*. Consider this attempt to implement a `fib(n)` function that's supposed to calculate [Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_sequence): 178 + 179 + ```js 180 + function fib(n) { 181 + if (n <= 1) { 182 + return n; 183 + } else { 184 + return fib(n) + fib(n - 1); 185 + } 186 + } 187 + ``` 188 + 189 + Actually, this function is buggy--it will hang forever. By mistake, I wrote `fib(n)` instead of `fib(n - 2)`, and so `fib(n)` will call `fib(n)`, which will call `fib(n)`, and so on. It will never get out of recursion because `n` doesn't ever "get smaller". 190 + 191 + Languages that understand *well-founded recursion* won't let me do this mistake. For example, in Lean, [this would have been a type error](https://live.lean-lang.org/?from=lean#codez=CYUwZgBGCWBGEAoB2EBcEByBDALgSjU1zQF4AoCCaSFQEyIIBGCHACxCQsog8pABsAziE6UY8FAGoocRCgC0EAEx4yZIA): 192 + 193 + ```lean 194 + def fib (n : Nat) : Nat := /- error: fail to show termination for fib -/ 195 + if n ≤ 1 then 196 + n 197 + else 198 + fib n + fib (n - 2) 199 + ``` 200 + 201 + Lean knows that `n` "isn't getting smaller" ([see here more precisely](https://lean-lang.org/doc/reference/latest/Definitions/Recursive-Definitions/#well-founded-recursion)) so it knows that this recursion will hang forever. It doesn't "get closer with time". 202 + 203 + This is not a Lean tutorial but I hope you'll forgive this frivolous metaphor. 204 + 205 + I think it's the same with the process of reducing the repro case. You want to know that *you're always, **always** making incremental progress* and the repro keeps getting smaller. This means that you must stay disciplined and remove pieces bit by bit, only committing as long as the bug still persists. At some point, you're bound to run out of things to remove, which would either present you with a mistake in your code, or a mistake in pieces you can't further reduce (e.g. React). 206 + 207 + Repeat until you find it. 208 + 209 + --- 210 + 211 + ## Step 4: Find the Root Cause 212 + 213 + Claude didn't end up solving this one, but it got me very close. 214 + 215 + After I told it to actually follow my instructions, and to only *remove* things, it removed enough code that the problem was contained in a single file. I moved that file outside the router, and suddenly the same code worked. Then I moved it back into the router, and it broke again. Then I made it a *top-level* route and it worked. 216 + 217 + Something was breaking when it was nested inside the root layout. 218 + 219 + My root layout looked like this: 220 + 221 + ```js 222 + import { Outlet, ScrollRestoration } from "react-router-dom"; 223 + 224 + export function RootLayout() { 225 + return ( 226 + <div> 227 + <ScrollRestoration /> 228 + <Outlet /> 229 + </div> 230 + ); 231 + } 232 + ``` 233 + 234 + Aha. It turns out, [there used to be a bug](https://github.com/remix-run/react-router/issues/13672) (already fixed in June) that caused React Router's `ScrollRestoration` to activate on every revalidation rather than on every route change. Since my network call (via an action) revalidated the route, it triggered `ScrollRestoration` during `scrollIntoView`, causing the jitter. 235 + 236 + **This exact workflow--removing things one by one while ensuring the bug is still present--saved my ass many times.** (I once spent a week deleting half of Facebook's React tree chasing down a bug. The final repro was ~50 lines of code.) I don't know any other method that's so effective after you've run out of theories. 237 + 238 + If I was setting up the project myself, I'd use the latest version of React Router and wouldn't have run into this bug. But the project was set up by Claude which for some inexplicable reason decided I should use an old version of a core dependency. 239 + 240 + Ah well! 241 + 242 + The joys of vibecoding.
+2 -4
public/impossible-components/client.js
··· 171 171 className="block border-2 px-1 mb-4" 172 172 /> 173 173 </div> 174 - <ul className="unstyled gap-2 flex flex-col"> 174 + <ul className="gap-2 flex flex-col"> 175 175 {sortedItems.map((item) => ( 176 - <li className="unstyled" key={item.id}> 177 - {item.content} 178 - </li> 176 + <li key={item.id}>{item.content}</li> 179 177 ))} 180 178 </ul> 181 179 </>
+1 -1
public/impossible-components/index.md
··· 1143 1143 1144 1144 ### A Note on Terminology 1145 1145 1146 - As in my other [recent](https://overreacted.io/react-for-two-computers/) [articles](https://overreacted.io/jsx-over-the-wire/), I've tried to avoid using the "Server Components" and "Client Components" terminology in this post because it brings up distracting connotations and knee-jerk reactions. (In particular, people tend to assume the "client loads from the server" rather than the "server renders the client" model.) 1146 + As in my other [recent](/react-for-two-computers/) [articles](/jsx-over-the-wire/), I've tried to avoid using the "Server Components" and "Client Components" terminology in this post because it brings up distracting connotations and knee-jerk reactions. (In particular, people tend to assume the "client loads from the server" rather than the "server renders the client" model.) 1147 1147 1148 1148 The "backend components" in this post are officially called Server Components, and the "frontend components" are officially called Client Components. If I could change the official terminology, I probably still would *not.* However, I find that introducing it when you already understand the model (as I hope you do by this point) works better than starting with the terminology. This may eventually stop being a problem if the Server/Client split as modeled by React Server Components ever becomes the generally accepted model of describing distributed composable user interfaces. I think we may get there at some point within the next ten years. 1149 1149
+15 -3
public/impossible-components/server.js
··· 62 62 return ( 63 63 <section className="rounded-md bg-black/5 p-2"> 64 64 <h5 className="font-bold"> 65 - <a href={"/" + slug} target="_blank"> 65 + <a 66 + href={"/" + slug} 67 + target="_blank" 68 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 69 + > 66 70 {data.title} 67 71 </a> 68 72 </h5> ··· 80 84 return ( 81 85 <section className="rounded-md bg-black/5 p-2"> 82 86 <h5 className="font-bold"> 83 - <a href={"/" + slug} target="_blank"> 87 + <a 88 + href={"/" + slug} 89 + target="_blank" 90 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 91 + > 84 92 {data.title} 85 93 </a> 86 94 </h5> ··· 103 111 } 104 112 > 105 113 <h5 className="font-bold"> 106 - <a href={"/" + slug} target="_blank"> 114 + <a 115 + href={"/" + slug} 116 + target="_blank" 117 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 118 + > 107 119 {data.title} 108 120 </a> 109 121 </h5>
+166
public/introducing-rsc-explorer/index.md
··· 1 + --- 2 + title: Introducing RSC Explorer 3 + date: '2025-12-19' 4 + spoiler: My new hobby project. 5 + bluesky: https://bsky.app/profile/danabra.mov/post/3mabn2f236s2f 6 + --- 7 + 8 + In the past few weeks, since the disclosure of the [critical security vulnerability in React Server Components (RSC)](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components), there's been a lot of interest in the RSC protocol. 9 + 10 + The RSC protocol is the format in which React trees (and a [superset of JSON](https://github.com/facebook/react/issues/25687)) get serialized and deserialized by React. React provides both a writer and a reader for the RSC protocol, which are versioned and evolved in lockstep with each other. 11 + 12 + Because the RSC protocol is an *implementation detail* of React, it is not explicitly documented outside the source code. The benefit of this approach is that React has a lot of leeway to improve the format and add new features and optimizations to it. 13 + 14 + However, the downside is that even people who actively build apps with React Server Components often don't have an intuition for how it works under the hood. 15 + 16 + A few months ago, I wrote [Progressive JSON](/progressive-json/) to explain some of the ideas used by the RSC protocol. While you don't "need" to know them to use RSC, I think it's one of the cases where looking under the hood is actually quite fun and instructive. 17 + 18 + I wish the circumstances around the increased interest now were different, but in any case, **that interest inspired me to make a new little tool** to show how it works. 19 + 20 + I'm calling it **RSC Explorer**, and you can find it at [`https://rscexplorer.dev/`](https://rscexplorer.dev/). 21 + 22 + Obviously, it's [open](https://tangled.org/danabra.mov/rscexplorer) [source](https://github.com/gaearon/rscexplorer). 23 + 24 + --- 25 + 26 + "Show, don't tell", as they say. Well, there it is as an embed. 27 + 28 + Let's start with the Hello World: 29 + 30 + <iframe 31 + style={{ width: "100%", height: 500, border: "1px solid #eee", borderRadius: 8 }} 32 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJleHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBBcHAoKSB7XG4gIHJldHVybiA8aDE%2BSGVsbG8gV29ybGQ8L2gxPlxufSIsImNsaWVudCI6Iid1c2UgY2xpZW50JyJ9" 33 + /> 34 + 35 + Notice there's a yellow highlighted line that says something cryptic. If you look closely, it's `<h1>Hello</h1>` represented as a piece of JSON. This line is a part of the RSC stream from the server. **That's how React talks to itself over the network.** 36 + 37 + **Now press the big yellow "step" button!** 38 + 39 + Notice how `<h1>Hello</h1>` now appears on the right. This is the JSX that the *client* reconstructs after reading this line. We've just seen a simple piece of JSX--the `<h1>Hello</h1>` tag--cross the network and get revived on the other side. 40 + 41 + Well, not *really* "cross the network". 42 + 43 + One cool thing about RSC Explorer is that it's a single-page app, i.e. **it runs entirely in your browser** (more precisely, the Server part runs in a worker). This is why, if you check the Network tab, you'll see no requests. So in a sense it's a simulation. 44 + 45 + Nevertheless, RSC Explorer is built using exactly the same packages that React provides to read and write the RSC protocol, so every line of the output is real. 46 + 47 + --- 48 + 49 + ## Async Component 50 + 51 + Let's try something slightly more interesting to see *streaming* in action. 52 + 53 + Take this example and press the big yellow "step" button **exactly two times:** 54 + 55 + <iframe 56 + style={{ width: "100%", height: 800, border: "1px solid #eee", borderRadius: 8 }} 57 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBTdXNwZW5zZSB9IGZyb20gJ3JlYWN0J1xuXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBBcHAoKSB7XG4gIHJldHVybiAoXG4gICAgPGRpdj5cbiAgICAgIDxoMT5Bc3luYyBDb21wb25lbnQ8L2gxPlxuICAgICAgPFN1c3BlbnNlIGZhbGxiYWNrPXs8cD5Mb2FkaW5nLi4uPC9wPn0%2BXG4gICAgICAgIDxTbG93Q29tcG9uZW50IC8%2BXG4gICAgICA8L1N1c3BlbnNlPlxuICAgIDwvZGl2PlxuICApXG59XG5cbmFzeW5jIGZ1bmN0aW9uIFNsb3dDb21wb25lbnQoKSB7XG4gIGF3YWl0IG5ldyBQcm9taXNlKHIgPT4gc2V0VGltZW91dChyLCA1MDApKVxuICByZXR1cm4gPHA%2BRGF0YSBsb2FkZWQhPC9wPlxufSIsImNsaWVudCI6Iid1c2UgY2xpZW50JyJ9" 58 + /> 59 + 60 + (If you miscounted, press "restart" on the left, and then "step" two times again.) 61 + 62 + Have a look at the upper right pane. You can see three chunks in the RSC protocol format (which, again, you don't technically *need* to read--and which changes between versions). On the right, you see what Client React reconstructed *so far*. 63 + 64 + **Notice a "hole" in the middle of the streamed tree, visualized as a "Pending" pill.** 65 + 66 + By default, React would not show an inconsistent UI with "holes". However, since you've declared a loading state with `<Suspense>`, a partially completed UI now can be displayed (notice how the `<h1>` is already visible but `<Suspense>` shows the fallback content because `<SlowComponent />` has not streamed in yet). 67 + 68 + Press the "step" button once again, and the "hole" will be filled. 69 + 70 + --- 71 + 72 + ## Counter 73 + 74 + So far, we've only sent *data* to the client; now let's also send some *code*. 75 + 76 + Let's use a counter as the classic example. 77 + 78 + Press the big yellow "step" button twice: 79 + 80 + <iframe 81 + style={{ width: "100%", height: 800, border: "1px solid #eee", borderRadius: 8 }} 82 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBDb3VudGVyIH0gZnJvbSAnLi9jbGllbnQnXG5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEFwcCgpIHtcbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPGgxPkNvdW50ZXI8L2gxPlxuICAgICAgPENvdW50ZXIgaW5pdGlhbENvdW50PXswfSAvPlxuICAgIDwvZGl2PlxuICApXG59IiwiY2xpZW50IjoiJ3VzZSBjbGllbnQnXG5cbmltcG9ydCB7IHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5cbmV4cG9ydCBmdW5jdGlvbiBDb3VudGVyKHsgaW5pdGlhbENvdW50IH0pIHtcbiAgY29uc3QgW2NvdW50LCBzZXRDb3VudF0gPSB1c2VTdGF0ZShpbml0aWFsQ291bnQpXG5cbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPHA%2BQ291bnQ6IHtjb3VudH08L3A%2BXG4gICAgICA8ZGl2IHN0eWxlPXt7IGRpc3BsYXk6ICdmbGV4JywgZ2FwOiA4IH19PlxuICAgICAgICA8YnV0dG9uIG9uQ2xpY2s9eygpID0%2BIHNldENvdW50KGMgPT4gYyAtIDEpfT7iiJI8L2J1dHRvbj5cbiAgICAgICAgPGJ1dHRvbiBvbkNsaWNrPXsoKSA9PiBzZXRDb3VudChjID0%2BIGMgKyAxKX0%2BKzwvYnV0dG9uPlxuICAgICAgPC9kaXY%2BXG4gICAgPC9kaXY%2BXG4gIClcbn0ifQ%3D%3D" 83 + /> 84 + 85 + That's just a good old counter, nothing too interesting here. 86 + 87 + Or is there? 88 + 89 + Have a look at the protocol payload. It's a bit tricky to read, but notice that we're not sending the string `"Count: 0"` or the `<button>`s, or any HTML. We're sending **`<Counter initialCount={0} />` itself--the "virtual DOM".** It can, of course, be turned to HTML later, just like any JSX can, but it doesn't have to be. 90 + 91 + It's like we're returning React trees from API routes. 92 + 93 + Notice how the `Counter` reference becomes `["client",[],"Counter"]` in the RSC protocol, which says "grab the `Counter` export from the `client` module". In a real framework, this would be done by the bundler, which is why RSC integrates with bundlers. If you're familiar with webpack, this is similar to reading from the webpack require cache. (In fact, [that's how](https://github.com/gaearon/rscexplorer/blob/58cee712d9223675d2c0e2c5b828b499150c2269/src/shared/webpack-shim.ts) RSC Explorer implements that.) 94 + 95 + --- 96 + 97 + ## Form Action 98 + 99 + We've just seen the server *referring* to a piece of code exposed by the client. 100 + 101 + Now let's see the client *referring* to a piece of code exposed by the server. 102 + 103 + Here, `greet` is a *Server Action*, exposed with `'use server'` as an endpoint. It's passed as a prop to the Client `Form` component that sees it as an `async` function. 104 + 105 + Press the big yellow "step" button three times: 106 + 107 + <iframe 108 + style={{ width: "100%", height: 900, border: "1px solid #eee", borderRadius: 8 }} 109 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBGb3JtIH0gZnJvbSAnLi9jbGllbnQnXG5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEFwcCgpIHtcbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPGgxPkZvcm0gQWN0aW9uPC9oMT5cbiAgICAgIDxGb3JtIGdyZWV0QWN0aW9uPXtncmVldH0gLz5cbiAgICA8L2Rpdj5cbiAgKVxufVxuXG5hc3luYyBmdW5jdGlvbiBncmVldChwcmV2U3RhdGUsIGZvcm1EYXRhKSB7XG4gICd1c2Ugc2VydmVyJ1xuICBhd2FpdCBuZXcgUHJvbWlzZShyID0%2BIHNldFRpbWVvdXQociwgNTAwKSlcbiAgY29uc3QgbmFtZSA9IGZvcm1EYXRhLmdldCgnbmFtZScpXG4gIGlmICghbmFtZSkgcmV0dXJuIHsgbWVzc2FnZTogbnVsbCwgZXJyb3I6ICdQbGVhc2UgZW50ZXIgYSBuYW1lJyB9XG4gIHJldHVybiB7IG1lc3NhZ2U6IGBIZWxsbywgJHtuYW1lfSFgLCBlcnJvcjogbnVsbCB9XG59IiwiY2xpZW50IjoiJ3VzZSBjbGllbnQnXG5cbmltcG9ydCB7IHVzZUFjdGlvblN0YXRlIH0gZnJvbSAncmVhY3QnXG5cbmV4cG9ydCBmdW5jdGlvbiBGb3JtKHsgZ3JlZXRBY3Rpb24gfSkge1xuICBjb25zdCBbc3RhdGUsIGZvcm1BY3Rpb24sIGlzUGVuZGluZ10gPSB1c2VBY3Rpb25TdGF0ZShncmVldEFjdGlvbiwge1xuICAgIG1lc3NhZ2U6IG51bGwsXG4gICAgZXJyb3I6IG51bGxcbiAgfSlcblxuICByZXR1cm4gKFxuICAgIDxmb3JtIGFjdGlvbj17Zm9ybUFjdGlvbn0%2BXG4gICAgICA8ZGl2IHN0eWxlPXt7IGRpc3BsYXk6ICdmbGV4JywgZ2FwOiA4IH19PlxuICAgICAgICA8aW5wdXRcbiAgICAgICAgICBuYW1lPVwibmFtZVwiXG4gICAgICAgICAgcGxhY2Vob2xkZXI9XCJFbnRlciB5b3VyIG5hbWVcIlxuICAgICAgICAgIHN0eWxlPXt7IGZsZXg6IDEsIG1pbldpZHRoOiAwLCBwYWRkaW5nOiAnOHB4IDEycHgnLCBib3JkZXJSYWRpdXM6IDQsIGJvcmRlcjogJzFweCBzb2xpZCAjY2NjJyB9fVxuICAgICAgICAvPlxuICAgICAgICA8YnV0dG9uIGRpc2FibGVkPXtpc1BlbmRpbmd9PlxuICAgICAgICAgIHtpc1BlbmRpbmcgPyAnU2VuZGluZy4uLicgOiAnR3JlZXQnfVxuICAgICAgICA8L2J1dHRvbj5cbiAgICAgIDwvZGl2PlxuICAgICAge3N0YXRlLmVycm9yICYmIDxwIHN0eWxlPXt7IGNvbG9yOiAncmVkJywgbWFyZ2luVG9wOiA4IH19PntzdGF0ZS5lcnJvcn08L3A%2BfVxuICAgICAge3N0YXRlLm1lc3NhZ2UgJiYgPHAgc3R5bGU9e3sgY29sb3I6ICdncmVlbicsIG1hcmdpblRvcDogOCB9fT57c3RhdGUubWVzc2FnZX08L3A%2BfVxuICAgIDwvZm9ybT5cbiAgKVxufSJ9" 110 + /> 111 + 112 + Now enter your name in the Preview pane and press "Greet". The RSC Explorer debugger will "pause" again, showing we've hit the `greet` Server Action with a request. Press the yellow "step" button to see the response returned to the client. 113 + 114 + --- 115 + 116 + ## Router Refresh 117 + 118 + RSC is often taught with a framework, but that obscures what's happening. For example, how does a framework refresh server content? How does a router work? 119 + 120 + RSC Explorer shows **frameworkless RSC.** There's no `router.refresh`--but you can implement your own `refresh` Server Action and a `Router` Component. 121 + 122 + Press the "step" button repeatedly to get the whole initial UI on the screen: 123 + 124 + <iframe 125 + style={{ width: "100%", height: 800, border: "1px solid #eee", borderRadius: 8 }} 126 + src="https://rscexplorer.dev/embed.html?c=eyJzZXJ2ZXIiOiJpbXBvcnQgeyBTdXNwZW5zZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgVGltZXIsIFJvdXRlciB9IGZyb20gJy4vY2xpZW50J1xuXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBBcHAoKSB7XG4gIHJldHVybiAoXG4gICAgPGRpdj5cbiAgICAgIDxoMT5Sb3V0ZXIgUmVmcmVzaDwvaDE%2BXG4gICAgICA8cCBzdHlsZT17eyBtYXJnaW5Cb3R0b206IDEyLCBjb2xvcjogJyM2NjYnIH19PlxuICAgICAgICBDbGllbnQgc3RhdGUgcGVyc2lzdHMgYWNyb3NzIHNlcnZlciBuYXZpZ2F0aW9uc1xuICAgICAgPC9wPlxuICAgICAgPFN1c3BlbnNlIGZhbGxiYWNrPXs8cD5Mb2FkaW5nLi4uPC9wPn0%2BXG4gICAgICAgIDxSb3V0ZXIgaW5pdGlhbD17cmVuZGVyUGFnZSgpfSByZWZyZXNoQWN0aW9uPXtyZW5kZXJQYWdlfSAvPlxuICAgICAgPC9TdXNwZW5zZT5cbiAgICA8L2Rpdj5cbiAgKVxufVxuXG5hc3luYyBmdW5jdGlvbiByZW5kZXJQYWdlKCkge1xuICAndXNlIHNlcnZlcidcbiAgcmV0dXJuIDxDb2xvclRpbWVyIC8%2BXG59XG5cbmFzeW5jIGZ1bmN0aW9uIENvbG9yVGltZXIoKSB7XG4gIGF3YWl0IG5ldyBQcm9taXNlKHIgPT4gc2V0VGltZW91dChyLCAzMDApKVxuICBjb25zdCBodWUgPSBNYXRoLmZsb29yKE1hdGgucmFuZG9tKCkgKiAzNjApXG4gIHJldHVybiA8VGltZXIgY29sb3I9e2Boc2woJHtodWV9LCA3MCUsIDg1JSlgfSAvPlxufSIsImNsaWVudCI6Iid1c2UgY2xpZW50J1xuXG5pbXBvcnQgeyB1c2VTdGF0ZSwgdXNlRWZmZWN0LCB1c2VUcmFuc2l0aW9uLCB1c2UgfSBmcm9tICdyZWFjdCdcblxuZXhwb3J0IGZ1bmN0aW9uIFRpbWVyKHsgY29sb3IgfSkge1xuICBjb25zdCBbc2Vjb25kcywgc2V0U2Vjb25kc10gPSB1c2VTdGF0ZSgwKVxuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgY29uc3QgaWQgPSBzZXRJbnRlcnZhbCgoKSA9PiBzZXRTZWNvbmRzKHMgPT4gcyArIDEpLCAxMDAwKVxuICAgIHJldHVybiAoKSA9PiBjbGVhckludGVydmFsKGlkKVxuICB9LCBbXSlcblxuICByZXR1cm4gKFxuICAgIDxkaXYgc3R5bGU9e3tcbiAgICAgIGJhY2tncm91bmQ6IGNvbG9yLFxuICAgICAgcGFkZGluZzogMjQsXG4gICAgICBib3JkZXJSYWRpdXM6IDgsXG4gICAgICB0ZXh0QWxpZ246ICdjZW50ZXInXG4gICAgfX0%2BXG4gICAgICA8cCBzdHlsZT17eyBmb250RmFtaWx5OiAnbW9ub3NwYWNlJywgZm9udFNpemU6IDMyLCBtYXJnaW46IDAgfX0%2BVGltZXI6IHtzZWNvbmRzfXM8L3A%2BXG4gICAgPC9kaXY%2BXG4gIClcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFJvdXRlcih7IGluaXRpYWwsIHJlZnJlc2hBY3Rpb24gfSkge1xuICBjb25zdCBbY29udGVudFByb21pc2UsIHNldENvbnRlbnRQcm9taXNlXSA9IHVzZVN0YXRlKGluaXRpYWwpXG4gIGNvbnN0IFtpc1BlbmRpbmcsIHN0YXJ0VHJhbnNpdGlvbl0gPSB1c2VUcmFuc2l0aW9uKClcbiAgY29uc3QgY29udGVudCA9IHVzZShjb250ZW50UHJvbWlzZSlcblxuICBjb25zdCByZWZyZXNoID0gKCkgPT4ge1xuICAgIHN0YXJ0VHJhbnNpdGlvbigoKSA9PiB7XG4gICAgICBzZXRDb250ZW50UHJvbWlzZShyZWZyZXNoQWN0aW9uKCkpXG4gICAgfSlcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPGRpdiBzdHlsZT17eyBvcGFjaXR5OiBpc1BlbmRpbmcgPyAwLjcgOiAxIH19PlxuICAgICAgPGJ1dHRvbiBvbkNsaWNrPXtyZWZyZXNofSBkaXNhYmxlZD17aXNQZW5kaW5nfSBzdHlsZT17eyBtYXJnaW5Cb3R0b206IDEyIH19PlxuICAgICAgICB7aXNQZW5kaW5nID8gJ1JlZnJlc2hpbmcuLi4nIDogJ1JlZnJlc2gnfVxuICAgICAgPC9idXR0b24%2BXG4gICAgICB7Y29udGVudH1cbiAgICA8L2Rpdj5cbiAgKVxufSJ9" 127 + /> 128 + 129 + Look at the ticking timer. Notice how the `ColorTimer` component from the Server passed a random color to the `Timer` component on the Client. Again, the Server has *returned* `<Timer color="hsl(96, 70%, 85%)" />` (or such). 130 + 131 + **Now press the Refresh button right above the timer.** 132 + 133 + Without digging into the code, "step" through the server response and see what happens. You should see a continously ticking `Timer` *receive new props from the server*. **Its background color will change but its state will be preserved!** 134 + 135 + In a sense, it's like refetching HTML using something like htmx, except it's a normal React "virtual DOM" update, so it doesn't destroy state. It's just receiving new props... from the server. Press "Refresh" a few times and step through it. 136 + 137 + If you want to look how this works under the hood, scroll down both Server and Client parts. In short, the Client `Router` keeps a Promise to the server JSX, which is returned by `renderPage()`. Initially, `renderPage()` is called on the Server (for the first render output), and later, it is called from the Client (for refetches). 138 + 139 + This technique, combined with URL matching and nesting, is pretty much how RSC frameworks handle routing. I think that's a pretty cool example! 140 + 141 + --- 142 + 143 + ## What Else? 144 + 145 + I've made a few more examples for the curious folks: 146 + 147 + - [Pagination](https://rscexplorer.dev/?s=pagination) 148 + - [Error Handling](https://rscexplorer.dev/?s=errors) 149 + - [Client Reference](https://rscexplorer.dev/?s=clientref) 150 + - [Bound Actions](https://rscexplorer.dev/?s=bound) 151 + - [Binary Data](https://rscexplorer.dev/?s=binary) 152 + - [Kitchen Sink](https://rscexplorer.dev/?s=kitchensink) 153 + 154 + And, of course, the infamous: 155 + 156 + - [CVE-2025-55182](https://rscexplorer.dev/?s=cve) 157 + 158 + (As you would expect, this one only works on the vulnerable versions so you'd need to select 19.2.0 in the top right corner to actually get it to work.) 159 + 160 + I'd love to see more cool RSC examples created by the community. 161 + 162 + RSC Explorer lets you embed snippets on other pages (as I've done in this post) and create sharable links as long as the code itself is not bigger than the URL limit. The tool is entirely client-side and I intend to keep it that way for simplicity. 163 + 164 + You're more than welcome to browse its source code on [Tangled](https://tangled.org/danabra.mov/rscexplorer) or [GitHub](https://github.com/gaearon/rscexplorer). This is a hobby project so I don't promise anything specific but I hope it's useful. 165 + 166 + Thank you for checking it out!
+3 -3
public/open-social/index.md
··· 171 171 172 172 ![Alice seamlessly moves her repository data to a different host.](./14-full.svg) 173 173 174 - *(This requires a modicum of technical skill today but it's getting [more accessible](https://pdsmoover.com/info.html).)* 174 + *(This requires a modicum of technical skill today but it's getting [more accessible](https://pdsmoover.com/info).)* 175 175 176 176 Just like with moving a personal site, changing where her repo is being served from doesn't require cooperation from the previous host. It also doesn't disrupt her ability to log into apps and doesn't break any links. The web repairs itself: 177 177 ··· 201 201 202 202 You get the idea. 203 203 204 - Over time, your repo grows to be a collection of data from different open social apps. This data is open by default--if you wanted to look at my Bluesky posts, or Tangled stars, or Leaflet publications, you wouldn't need to hit these applications' APIs. You could just [hit my personal repository and enumerate all of its records](https://atproto-browser.vercel.app/at/danabra.mov). 204 + Over time, your repo grows to be a collection of data from different open social apps. This data is open by default--if you wanted to look at my Bluesky posts, or Tangled stars, or Leaflet publications, you wouldn't need to hit these applications' APIs. You could just [hit my personal repository and enumerate all of its records](https://pdsls.dev/at://danabra.mov). 205 205 206 206 To avoid naming collisions, the data in the repository is grouped by the format: 207 207 ··· 271 271 272 272 An important detail is that commits are cryptographically signed, which means that you don't need to trust a relay or a cache of network data. You can verify that the records haven't been tampered with, and each commit is legitimate. This is why "AT" in "AT Protocol" stands for "authenticated transfer". You're supposed to pronounce it like “@" ("at") though. Don't say "ay-tee" or you'll embarrass me! 273 273 274 - As time goes by, we'll see more infrastructure built around and for open social apps. [Graze](https://www.graze.social/) is letting users build their own algorithmic feeds, and [Slices](https://slices.network/) is an upcoming developer platform that does large-scale repository indexing for you. [Constellation](https://constellation.microcosm.blue/) and [If This Then AT://](https://app.ifthisthen.at/) offer easy network querying and automation. 274 + As time goes by, we'll see more infrastructure built around and for open social apps. [Graze](https://www.graze.social/) is letting you build algorithmic feeds, [Quickslice](https://quickslice.slices.network/) and [Tap](https://docs.bsky.app/blog/introducing-tap) simplify indexing, [Constellation](https://constellation.microcosm.blue/) and [If This Then AT://](https://app.ifthisthen.at/) provide querying and automation. 275 275 276 276 These are all technical details, though. 277 277
+1 -1
public/react-as-a-ui-runtime/index.md
··· 1083 1083 1084 1084 **Of course, `use` is not actually a syntax.** (It wouldn’t bring much benefit and would create a lot of friction.) 1085 1085 1086 - However, React *does* expect that all calls to Hooks happen only at the top level of a component and unconditionally. These [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) can be enforced with [a linter plugin](https://www.npmjs.com/package/eslint-plugin-react-hooks). There have been heated arguments about this design choice but in practice, I haven’t seen it confusing people. I also wrote about why commonly proposed alternatives [don’t work](https://overreacted.io/why-do-hooks-rely-on-call-order/). 1086 + However, React *does* expect that all calls to Hooks happen only at the top level of a component and unconditionally. These [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) can be enforced with [a linter plugin](https://www.npmjs.com/package/eslint-plugin-react-hooks). There have been heated arguments about this design choice but in practice, I haven’t seen it confusing people. I also wrote about why commonly proposed alternatives [don’t work](/why-do-hooks-rely-on-call-order/). 1087 1087 1088 1088 Internally, Hooks are implemented as [linked lists](https://dev.to/aspittel/thank-u-next-an-introduction-to-linked-lists-4pph). When you call `useState`, we move the pointer to the next item. When we exit the component’s [“call tree” frame](#call-tree), we save the resulting list there until the next render. 1089 1089
+1 -1
public/the-math-is-haunted/index.md
··· 30 30 31 31 To a mathematician's eye, this syntax looks like stating a theorem. We have the `theorem` keyword, the name of our theorem, a colon `:` before its statement, the statement that we'd like to prove, and `:= by` followed by the proof (`sorry` means that we haven't completed the actual proof yet but we're planning to fill it in later). 32 32 33 - But if you're a programmer, you might notice a hint of something else. That `theorem` looks suspiciously like a function. But then what is `2 = 2`? It looks like a return type of that function. But how can `2 = 2` be a *type*? Isn't `2 = 2` just a boolean? And if `2 = 2` really *is* a type, what are the *values* of that `2 = 2` type? [These are very interesting questions](https://overreacted.io/beyond-booleans/), but we'll have to forget about them for now. 33 + But if you're a programmer, you might notice a hint of something else. That `theorem` looks suspiciously like a function. But then what is `2 = 2`? It looks like a return type of that function. But how can `2 = 2` be a *type*? Isn't `2 = 2` just a boolean? And if `2 = 2` really *is* a type, what are the *values* of that `2 = 2` type? [These are very interesting questions](/beyond-booleans/), but we'll have to forget about them for now. 34 34 35 35 Instead, we'll start by inspecting the proof: 36 36
+5 -1
public/the-two-reacts/post-preview.js
··· 9 9 return ( 10 10 <section className="rounded-md bg-black/5 p-2"> 11 11 <h5 className="font-bold"> 12 - <a href={"/" + slug} target="_blank"> 12 + <a 13 + href={"/" + slug} 14 + target="_blank" 15 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 16 + > 13 17 {data.title} 14 18 </a> 15 19 </h5>
+7 -3
public/where-its-at/index.md
··· 46 46 47 47 ![at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z](./2-full.svg) 48 48 49 - An easy way to resolve an `at://` URI is using an [SDK](https://sdk.blue/) or a client app. Let's try an online client, for example, [pdsls](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) or [Taproot](https://atproto.at/viewer?uri=at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) or [atproto-browser](https://atproto-browser.vercel.app/at/ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z). They'll figure out the physical server where its JSON is currently hosted, and show that JSON for you. 49 + An easy way to resolve an `at://` URI is to use an [SDK](https://sdk.blue/) or a client app. Let's try an online client, for example, [pdsls](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) or [Taproot](https://atproto.at/viewer?uri=at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) or [atproto-browser](https://atproto-browser.vercel.app/at/ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z). They'll figure out the physical server where its JSON is currently hosted, and show that JSON for you. 50 50 51 51 **The above `at://` URI points at this JSON, wherever it is currently being hosted:** 52 52 ··· 355 355 356 356 For example, the `@ruuuuu.de` handle resolves to `did:web:iam.ruuuuu.de`, and its DID Document has a `serviceEndpoint` pointing at `https://blacksky.app`. 357 357 358 - To get the [`at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z`](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z) record, hit the `https://blacksky.app` server with the [`com.atproto.repo.getRecord`](https://docs.bsky.app/docs/api/com-atproto-repo-get-record) endpoint, passing different parts of the `at://` URI as parameters: 358 + To get the <span style={{wordBreak: 'break-word'}}>[`at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z`](https://pdsls.dev/at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z)</span> record, hit the `https://blacksky.app` server with the [`com.atproto.repo.getRecord`](https://docs.bsky.app/docs/api/com-atproto-repo-get-record) endpoint, passing different parts of the `at://` URI as parameters: 359 359 360 360 ```sh 361 361 $ curl "https://blacksky.app/xrpc/com.atproto.repo.getRecord?\ ··· 379 379 } 380 380 ``` 381 381 382 - Now let's get [`at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22`](https://pdsls.dev/at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22): 382 + Now let's get <span style={{wordBreak: 'break-word'}}>[`at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22`](https://pdsls.dev/at://danabra.mov/sh.tangled.feed.star/3m23ddgjpgn22)</span>: 383 383 384 384 - The `@danabra.mov` handle resolves to `did:plc:fpruhuo22xkm5o7ttr2ktxdo`. 385 385 - The DID Document for `did:plc:fpruhuo22xkm5o7ttr2ktxdo` points at `https://morel.us-east.host.bsky.network` as the current hosting. ··· 407 407 408 408 And that's how you resolve an `at://` URI. 409 409 410 + *Exercise: In the record above, the `subject` is a link to another record. Figure out the handle of its owner and the contents of that record. Use [pdsls](https://pdsls.dev/) to check your answer.* 411 + 410 412 --- 411 413 412 414 ### In Conclusion ··· 418 420 3. Request the JSON from that hosting (by hitting it with `getRecord`). 419 421 420 422 If you're building a client app or a small project, an [SDK](https://sdk.blue/) will handle all of this for you. However, for good performance, you'll want to hit a resolution cache instead of doing DNS/HTTPS lookups on every request. [QuickDID](https://quickdid.smokesignal.tools/) is one such cache. You can also check out the [pdsls source](https://tangled.org/@pdsls.dev/pdsls/blob/main/src/utils/api.ts) to see how exactly it handles resolution. 423 + 424 + In practice, a lot of apps don't end up needing to resolve `at://` URIs or load JSON records because they *receive* data from the network [via a websocket](https://pdsls.dev/jetstream?instance=wss%3A%2F%2Fjetstream1.us-east.bsky.network%2Fsubscribe) and aggregate it in a local database. If that's your approach, you'll still use the `at://` URIs as unique identifiers for user-created data, but the data itself will get pushed to you rather than pulled by you. Still, it's useful to know that you *can* fetch it on demand. 421 425 422 426 The AT protocol is fundamentally an abstraction over HTTP, DNS, and JSON. But by standardizing how these pieces fit together—putting the user in the authority position, separating identity from hosting, and making data portable—it turns the web into a place where [your content belongs to you](/open-social/), not to the apps that display it. 423 427