my blog https://overreacted.io

simplify styling

+300 -222
+4 -3
app/Link.tsx
··· 27 ...rest 28 }: { 29 className?: string; 30 - children: React.ReactNode; 31 style?: React.CSSProperties; 32 href: string; 33 target?: string; 34 } & React.ComponentProps<typeof NextLink>) { 35 const router = useRouter(); 36 const [isNavigating, trackNavigation] = useTransition(); 37 - if (!target && !href.startsWith("/") && !href.startsWith("#")) { 38 target = "_blank"; 39 } 40 return ( ··· 50 }); 51 } 52 }} 53 - className={[className, `scale-100 active:scale-100`].join(" ")} 54 style={{ 55 ...style, 56 transform: isNavigating ? "scale(1)" : "",
··· 27 ...rest 28 }: { 29 className?: string; 30 + children?: React.ReactNode; 31 style?: React.CSSProperties; 32 href: string; 33 target?: string; 34 } & React.ComponentProps<typeof NextLink>) { 35 const router = useRouter(); 36 const [isNavigating, trackNavigation] = useTransition(); 37 + const isExternal = /^https?:\/\//.test(href); 38 + if (!target && isExternal) { 39 target = "_blank"; 40 } 41 return ( ··· 51 }); 52 } 53 }} 54 + className={[className, "scale-100 active:scale-100"].join(" ")} 55 style={{ 56 ...style, 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 return ( 5 <> 6 {children} 7 - <footer className="mt-12"> 8 <HomeLink /> 9 </footer> 10 </>
··· 4 return ( 5 <> 6 {children} 7 + <footer className="mt-20"> 8 <HomeLink /> 9 </footer> 10 </>
+29 -150
app/[slug]/markdown.css
··· 1 .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%; 100 - } 101 - 102 - .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; 109 - } 110 - 111 - .markdown input { 112 - color: #222; 113 - } 114 - 115 - .markdown table { 116 - width: 100%; 117 - border-collapse: collapse; 118 - margin-bottom: 1.5rem; 119 - } 120 - 121 - .markdown th, 122 - .markdown td { 123 - border: 1px solid #dcdcdc; 124 - padding: 8px; 125 - text-align: left; 126 - } 127 - 128 - @media (prefers-color-scheme: dark) { 129 - .markdown input { 130 - color: #111; 131 - } 132 - } 133 - 134 .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; 142 } 143 144 .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); 147 } 148 149 .tip.tip-sm { 150 - @apply inline-block px-6 py-2 text-lg shadow-md; 151 } 152 153 .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; 161 } 162 163 @media (prefers-color-scheme: dark) { 164 - .tip-bg { 165 - filter: brightness(0.46); 166 - } 167 }
··· 1 + /* CSS vars for code block customization (used by Wrapper components) */ 2 .markdown { 3 + line-height: 28px; 4 + --path: none; 5 + --radius-top: 12px; 6 + --radius-bottom: 12px; 7 + --padding-top: 1rem; 8 + --padding-bottom: 1rem; 9 } 10 11 + /* Code line highlighting - data-attribute from rehype-pretty-code */ 12 .markdown pre [data-highlighted-line] { 13 + margin-left: -16px; 14 + margin-right: -16px; 15 + padding-left: 12px; 16 + border-left: 4px solid #ffa7c4; 17 + background-color: #022a4b; 18 + display: block; 19 + padding-right: 1em; 20 } 21 22 + /* Tip button styles */ 23 .tip { 24 + @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; 25 + border-color: var(--link); 26 } 27 28 .tip.tip-sm { 29 + @apply inline-block px-6 py-2 text-lg shadow-md; 30 } 31 32 .tip-bg { 33 + background-image: linear-gradient(45deg, var(--purple), var(--pink)); 34 + position: absolute; 35 + top: 0; 36 + left: 0; 37 + right: 0; 38 + bottom: 0; 39 + z-index: -1; 40 } 41 42 @media (prefers-color-scheme: dark) { 43 + .tip-bg { 44 + filter: brightness(0.46); 45 + } 46 }
+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 import { readdir, readFile } from "fs/promises"; 3 import matter from "gray-matter"; 4 import { MDXRemote } from "next-mdx-remote-client/rsc"; 5 - import Link from "../Link"; 6 import { sans } from "../fonts"; 7 import remarkSmartpants from "remark-smartypants"; 8 import rehypePrettyCode from "rehype-pretty-code"; 9 import rehypeSlug from "rehype-slug"; 10 - import rehypeAutolinkHeadings from "rehype-autolink-headings"; 11 import { remarkMdxEvalCodeBlock } from "./mdx"; 12 import overnight from "overnight/themes/Overnight-Slumber.json"; 13 import "./markdown.css"; 14 import remarkGfm from "remark-gfm"; 15 16 overnight.colors["editor.background"] = "var(--code-bg)"; 17 ··· 54 year: "numeric", 55 })} 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> 80 81 <Wrapper> 82 <MDXRemote 83 source={content} 84 components={{ 85 - a: Link, 86 img: async ({ src, ...rest }) => { 87 if ( 88 src && ··· 121 finalSrc = `/${slug}/${src}`; 122 } 123 124 - return <img src={finalSrc} {...rest} />; 125 }, 126 Video: ({ src, ...rest }) => { 127 let finalSrc = src; ··· 146 rehypePrettyCode, 147 { 148 theme: overnight, 149 }, 150 ], 151 [rehypeSlug], 152 - [ 153 - rehypeAutolinkHeadings, 154 - { 155 - behavior: "wrap", 156 - properties: { 157 - className: "linked-heading", 158 - target: "_self", 159 - }, 160 - }, 161 - ], 162 ] as any, 163 } as any, 164 }} 165 /> 166 </Wrapper> 167 {!data.nocta && ( 168 - <div className="flex flex-wrap items-baseline"> 169 <a 170 href="https://ko-fi.com/gaearon" 171 target="_blank" 172 - className="tip mb-8 relative md:-left-8" 173 > 174 <span className="tip-bg" /> 175 Pay what you like 176 </a> 177 - <a 178 - className="leading-tight ml-4 relative md:-left-8" 179 - href="/hire-me-in-japan/" 180 - > 181 - Hire me 182 - </a> 183 </div> 184 )} 185 - <hr /> 186 <p> 187 {data.bluesky && ( 188 <> 189 - <Link href={data.bluesky}>Discuss on Bluesky</Link> 190 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 191 </> 192 )} 193 {data.youtube && ( 194 <> 195 - <Link href={data.youtube}>Watch on YouTube</Link> 196 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 197 </> 198 )} 199 {/* TODO: This should say Edit when Tangled adds an editor. */} 200 - <Link href={editUrl}>Fork on Tangled</Link> 201 </p> 202 </div> 203 </article>
··· 2 import { readdir, readFile } from "fs/promises"; 3 import matter from "gray-matter"; 4 import { MDXRemote } from "next-mdx-remote-client/rsc"; 5 + import TextLink from "../TextLink"; 6 import { sans } from "../fonts"; 7 import remarkSmartpants from "remark-smartypants"; 8 import rehypePrettyCode from "rehype-pretty-code"; 9 import rehypeSlug from "rehype-slug"; 10 import { remarkMdxEvalCodeBlock } from "./mdx"; 11 import overnight from "overnight/themes/Overnight-Slumber.json"; 12 import "./markdown.css"; 13 import remarkGfm from "remark-gfm"; 14 + import * as markdown from "./markdown"; 15 16 overnight.colors["editor.background"] = "var(--code-bg)"; 17 ··· 54 year: "numeric", 55 })} 56 </p> 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 + )} 78 79 <Wrapper> 80 + <div className="flex flex-col gap-8"> 81 <MDXRemote 82 source={content} 83 components={{ 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 + ), 101 img: async ({ src, ...rest }) => { 102 if ( 103 src && ··· 136 finalSrc = `/${slug}/${src}`; 137 } 138 139 + return <markdown.Img src={finalSrc} {...rest} />; 140 }, 141 Video: ({ src, ...rest }) => { 142 let finalSrc = src; ··· 161 rehypePrettyCode, 162 { 163 theme: overnight, 164 + defaultLang: { block: "text" }, 165 }, 166 ], 167 [rehypeSlug], 168 ] as any, 169 } as any, 170 }} 171 /> 172 + </div> 173 </Wrapper> 174 {!data.nocta && ( 175 + <div className="flex flex-wrap items-baseline gap-4 relative md:-left-8"> 176 <a 177 href="https://ko-fi.com/gaearon" 178 target="_blank" 179 + className="tip" 180 > 181 <span className="tip-bg" /> 182 Pay what you like 183 </a> 184 + <TextLink href="/hire-me-in-japan/">Hire me</TextLink> 185 </div> 186 )} 187 + <hr className="opacity-60 dark:opacity-10" /> 188 <p> 189 {data.bluesky && ( 190 <> 191 + <TextLink href={data.bluesky}>Discuss on Bluesky</TextLink> 192 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 193 </> 194 )} 195 {data.youtube && ( 196 <> 197 + <TextLink href={data.youtube}>Watch on YouTube</TextLink> 198 &nbsp;&nbsp;&middot;&nbsp;&nbsp; 199 </> 200 )} 201 {/* TODO: This should say Edit when Tangled adds an editor. */} 202 + <TextLink href={editUrl}>Fork on Tangled</TextLink> 203 </p> 204 </div> 205 </article>
+6 -6
public/a-complete-guide-to-useeffect/index.md
··· 89 } 90 ``` 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/). 93 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 ··· 140 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 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/).)* 144 145 ## Each Render Has Its Own Event Handlers 146 ··· 190 191 Go ahead and [try it yourself!](https://codesandbox.io/s/w2wxl3yo0l) 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. 194 195 The alert will “capture” the state at the time I clicked the button. 196 ··· 394 395 **Conceptually, you can imagine effects are a *part of the render result*.** 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. 398 399 --- 400 ··· 518 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 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/). 522 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 ··· 628 629 ## Synchronization, Not Lifecycle 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. 632 633 Say my component looks like this: 634
··· 89 } 90 ``` 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](/react-as-a-ui-runtime/). 93 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 ··· 140 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 143 + *(For an in-depth overview of this process, check out my post [React as a UI Runtime](/react-as-a-ui-runtime/).)* 144 145 ## Each Render Has Its Own Event Handlers 146 ··· 190 191 Go ahead and [try it yourself!](https://codesandbox.io/s/w2wxl3yo0l) 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](/how-are-function-components-different-from-classes/) explores the reasons in depth but the correct answer is 3. 194 195 The alert will “capture” the state at the time I clicked the button. 196 ··· 394 395 **Conceptually, you can imagine effects are a *part of the render result*.** 396 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 399 --- 400 ··· 518 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 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 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 ··· 628 629 ## Synchronization, Not Lifecycle 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](/the-bug-o-notation/) of your program. 632 633 Say my component looks like this: 634
+1 -1
public/hire-me-in-japan/index.md
··· 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](https://overreacted.io/open-social/). 65 66 #### 2015–2023: React at Meta/Facebook 67
··· 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
+1 -1
public/how-imports-work-in-rsc/index.md
··· 196 197 In that sense, when you `import` some code, you bring it *into* your program. 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)) 200 201 --- 202
··· 196 197 In that sense, when you `import` some code, you bring it *into* your program. 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?](/jsx-over-the-wire/#backend-for-frontend)) 200 201 --- 202
+2 -4
public/impossible-components/client.js
··· 171 className="block border-2 px-1 mb-4" 172 /> 173 </div> 174 - <ul className="unstyled gap-2 flex flex-col"> 175 {sortedItems.map((item) => ( 176 - <li className="unstyled" key={item.id}> 177 - {item.content} 178 - </li> 179 ))} 180 </ul> 181 </>
··· 171 className="block border-2 px-1 mb-4" 172 /> 173 </div> 174 + <ul className="gap-2 flex flex-col"> 175 {sortedItems.map((item) => ( 176 + <li key={item.id}>{item.content}</li> 177 ))} 178 </ul> 179 </>
+1 -1
public/impossible-components/index.md
··· 1143 1144 ### A Note on Terminology 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.) 1147 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
··· 1143 1144 ### A Note on Terminology 1145 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 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
+15 -3
public/impossible-components/server.js
··· 62 return ( 63 <section className="rounded-md bg-black/5 p-2"> 64 <h5 className="font-bold"> 65 - <a href={"/" + slug} target="_blank"> 66 {data.title} 67 </a> 68 </h5> ··· 80 return ( 81 <section className="rounded-md bg-black/5 p-2"> 82 <h5 className="font-bold"> 83 - <a href={"/" + slug} target="_blank"> 84 {data.title} 85 </a> 86 </h5> ··· 103 } 104 > 105 <h5 className="font-bold"> 106 - <a href={"/" + slug} target="_blank"> 107 {data.title} 108 </a> 109 </h5>
··· 62 return ( 63 <section className="rounded-md bg-black/5 p-2"> 64 <h5 className="font-bold"> 65 + <a 66 + href={"/" + slug} 67 + target="_blank" 68 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 69 + > 70 {data.title} 71 </a> 72 </h5> ··· 84 return ( 85 <section className="rounded-md bg-black/5 p-2"> 86 <h5 className="font-bold"> 87 + <a 88 + href={"/" + slug} 89 + target="_blank" 90 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 91 + > 92 {data.title} 93 </a> 94 </h5> ··· 111 } 112 > 113 <h5 className="font-bold"> 114 + <a 115 + href={"/" + slug} 116 + target="_blank" 117 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 118 + > 119 {data.title} 120 </a> 121 </h5>
+1 -1
public/react-as-a-ui-runtime/index.md
··· 1083 1084 **Of course, `use` is not actually a syntax.** (It wouldn’t bring much benefit and would create a lot of friction.) 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/). 1087 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
··· 1083 1084 **Of course, `use` is not actually a syntax.** (It wouldn’t bring much benefit and would create a lot of friction.) 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](/why-do-hooks-rely-on-call-order/). 1087 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
+1 -1
public/the-math-is-haunted/index.md
··· 30 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 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. 34 35 Instead, we'll start by inspecting the proof: 36
··· 30 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 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 35 Instead, we'll start by inspecting the proof: 36
+5 -1
public/the-two-reacts/post-preview.js
··· 9 return ( 10 <section className="rounded-md bg-black/5 p-2"> 11 <h5 className="font-bold"> 12 - <a href={"/" + slug} target="_blank"> 13 {data.title} 14 </a> 15 </h5>
··· 9 return ( 10 <section className="rounded-md bg-black/5 p-2"> 11 <h5 className="font-bold"> 12 + <a 13 + href={"/" + slug} 14 + target="_blank" 15 + className="underline decoration-[--link] decoration-1 underline-offset-4 text-[--link]" 16 + > 17 {data.title} 18 </a> 19 </h5>