# Animation with GSAP and React ## What is GSAP? [#what-is-gsap] **GSAP** (GreenSock Animation Platform) is a battle-tested JavaScript animation library capable of animating anything JavaScript can touch -DOM elements, CSS properties, SVG, canvas, Web Audio, and more. It runs on every browser, respects GPU compositing, and produces silky 60fps motion even on low-end devices. Unlike CSS keyframes or `motion/react`, GSAP excels at **sequenced, imperative timelines** -the kind of choreographed entrance animations where element A finishes, then B starts halfway through C. That control is difficult to express declaratively and is where GSAP shines. ### GSAP vs CSS vs Framer Motion [#gsap-vs-css-vs-framer-motion] | | CSS animations | Framer Motion | GSAP | | ----------------- | ------------------------------------ | -------------------- | -------------------- | | **Control** | Limited | Good | Precise | | **Sequencing** | `animation-delay` hacks | `variants` stagger | `Timeline` API | | **Scroll-driven** | `@scroll-timeline` (limited support) | `useScroll` | `ScrollTrigger` | | **Performance** | GPU (transform/opacity only) | GPU | GPU | | **Bundle size** | 0 kb | \~50 kb | \~30 kb (core) | | **Best for** | Simple CSS transitions | Declarative React UI | Complex choreography | *** ## Setup [#setup] Install GSAP and the official React integration: npm pnpm yarn bun ```bash npm install gsap @gsap/react ``` ```bash pnpm add gsap @gsap/react ``` ```bash yarn add gsap @gsap/react ``` ```bash bun add gsap @gsap/react ``` Register the `useGSAP` plugin once at the module level -this prevents double-registration in strict mode: ```tsx title="app/layout.tsx" import { gsap } from 'gsap'; import { useGSAP } from '@gsap/react'; gsap.registerPlugin(useGSAP); ``` *** ## Core concepts [#core-concepts] ### `gsap.to` vs `gsap.from` vs `gsap.fromTo` [#gsapto-vs-gsapfrom-vs-gsapfromto] These are the three animation primitives. The difference is simply where the animation *starts* and *ends*. ```tsx // Animate TO these values from whatever state the element is in gsap.to('.box', { opacity: 1, y: 0, duration: 0.6 }); // Animate FROM these values to the element's current CSS state gsap.from('.box', { opacity: 0, y: 40, duration: 0.6 }); // Explicitly define both start and end -most predictable gsap.fromTo( '.box', { opacity: 0, y: 40 }, // from { opacity: 1, y: 0, duration: 0.6 } // to ); ``` Prefer `gsap.fromTo` in production. `gsap.from` reads the element's computed style as the end state, which can produce surprising results when styles change after mount (e.g. dark mode, SSR hydration). ### Easing [#easing] GSAP ships a comprehensive easing suite. The most useful for UI work: ```tsx gsap.from('.box', { y: 40, ease: 'power3.out', duration: 0.5 }); // snappy entrance gsap.from('.box', { y: 40, ease: 'back.out(1.7)', duration: 0.5 }); // slight overshoot gsap.from('.box', { y: 40, ease: 'elastic.out(1, 0.4)', duration: 1 }); // springy gsap.from('.box', { y: 40, ease: 'expo.out', duration: 0.6 }); // very fast then slow ``` As a rule of thumb: use `power2.out` or `power3.out` for most UI entrances. Reserve `elastic` and `back` for playful, attention-grabbing moments. *** ## 1 - Fade In [#1---fade-in] The simplest GSAP animation: fade an element in from below on mount. ### How it works [#how-it-works] * `useGSAP` is the React-safe hook from `@gsap/react`. It handles cleanup automatically on unmount (no `useEffect` + `return () => gsap.killTweensOf(...)` boilerplate). * The `scope` option scopes all selector queries to `containerRef.current`, so `.fade-box` only matches elements *inside* that container -safe to reuse across multiple instances. * `gsap.from` starts from `opacity: 0, y: 40` and animates to the element's natural state. * A `replay` state counter is passed in `dependencies: [replay]` -incrementing it kills the previous tween and re-runs the animation. Hit **↺ Replay** to see it in action. ```tsx title="components/gsap-fade-in.tsx" 'use client'; import { useRef, useState } from 'react'; import { useGSAP } from '@gsap/react'; import { gsap } from 'gsap'; gsap.registerPlugin(useGSAP); export default function GsapFadeIn() { const containerRef = useRef(null); const [replay, setReplay] = useState(0); useGSAP( () => { gsap.from('.fade-box', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }); }, { scope: containerRef, dependencies: [replay] } ); return (
Fade In
); } ``` **Live result -hit ↺ Replay to restart:** *** ## 2 - Stagger [#2---stagger] Stagger staggers the start time of the same animation across multiple elements -creating a wave or cascade effect. ### How it works [#how-it-works-1] * `stagger: 0.1` means each subsequent `.stagger-item` starts 100ms after the previous one. * Stagger can also be an object: `stagger: { each: 0.1, from: 'center' }` to radiate outward from the middle. * `ease: 'back.out(1.7)'` adds a small overshoot on arrival -great for tags and chips. ```tsx gsap.from('.item', { opacity: 0, y: 30, scale: 0.85, stagger: 0.1, // 100ms between each ease: 'back.out(1.7)', duration: 0.5 }); // Advanced -radiate from center outward gsap.from('.item', { opacity: 0, stagger: { each: 0.08, from: 'center' } }); ``` ```tsx title="components/gsap-stagger.tsx" 'use client'; import { useRef, useState } from 'react'; import { useGSAP } from '@gsap/react'; import { gsap } from 'gsap'; gsap.registerPlugin(useGSAP); const ITEMS = ['React', 'GSAP', 'Next.js', 'TypeScript', 'Tailwind']; export default function GsapStagger() { const containerRef = useRef(null); const [replay, setReplay] = useState(0); useGSAP( () => { gsap.from('.stagger-item', { opacity: 0, y: 30, scale: 0.85, duration: 0.5, ease: 'back.out(1.7)', stagger: 0.1 }); }, { scope: containerRef, dependencies: [replay] } ); return (
{ITEMS.map((item) => (
{item}
))}
); } ``` **Live result -hit ↺ Replay to restart:** *** ## 3 - Timeline [#3---timeline] A `gsap.timeline()` lets you **chain multiple tweens in sequence**, with precise overlap control via position parameters. This is the killer feature that makes GSAP irreplaceable for choreographed UI. ### Position parameter [#position-parameter] The second argument to `tl.from()` / `tl.to()` is the **position parameter** -it controls where in the timeline the tween starts: ```tsx const tl = gsap.timeline(); tl.from('#avatar', { opacity: 0, scale: 0.5, duration: 0.4 }) // starts 0.1s before the previous tween ends .from('#name', { opacity: 0, x: -20 }, '-=0.1') // starts 0.15s before the previous tween ends .from('#role', { opacity: 0, x: -20 }, '-=0.15') // starts at absolute position 0.8s into the timeline .from('#badge', { opacity: 0, y: 10 }, '0.8') // starts immediately after a label called "reveal" .from('#cta', { opacity: 0 }, 'reveal') ``` | Position syntax | Meaning | | --------------- | ---------------------------------------- | | `'-=0.2'` | 0.2s before the *previous* tween ends | | `'+=0.1'` | 0.1s after the *previous* tween ends | | `'<'` | Same start time as the *previous* tween | | `'<0.1'` | 0.1s after the *previous* tween *starts* | | `0.5` | Absolute 0.5s from the timeline start | | `'label'` | At the named label | ### Profile card entrance [#profile-card-entrance] Below is a profile card where the avatar, name, role, and status badge animate in sequence with overlapping timing: ```tsx title="components/gsap-timeline.tsx" 'use client'; import { useRef, useState } from 'react'; import { useGSAP } from '@gsap/react'; import { gsap } from 'gsap'; gsap.registerPlugin(useGSAP); export default function GsapTimeline() { const containerRef = useRef(null); const avatarRef = useRef(null); const nameRef = useRef(null); const roleRef = useRef(null); const badgeRef = useRef(null); const [replay, setReplay] = useState(0); useGSAP( () => { const tl = gsap.timeline({ defaults: { ease: 'power3.out' } }); tl.from(avatarRef.current, { opacity: 0, scale: 0.5, duration: 0.45 }) .from(nameRef.current, { opacity: 0, x: -20, duration: 0.35 }, '-=0.1') .from(roleRef.current, { opacity: 0, x: -20, duration: 0.3 }, '-=0.15') .from(badgeRef.current, { opacity: 0, y: 10, scale: 0.8, duration: 0.3 }, '-=0.1'); }, { scope: containerRef, dependencies: [replay] } ); return (
B

Braswell Jr.

Software Engineer

Available
); } ``` **Live result -hit ↺ Replay to restart:** ### `clearProps: 'all'` [#clearprops-all] Always call `clearProps: 'all'` on entrance tweens when GSAP animates to a final state that is already expressed in CSS. Without it, GSAP leaves `opacity: 1; transform: translateY(0px)` as inline styles -which override your CSS and break things like dark mode transitions. ```tsx gsap.from('.hero', { opacity: 0, y: 32, duration: 0.5, clearProps: 'all' // removes inline styles when tween completes }); ``` *** ## 4 - ScrollTrigger [#4---scrolltrigger] `ScrollTrigger` is a GSAP plugin that fires animations based on the scroll position. It is far more powerful than CSS `@scroll-timeline` and works in every browser today. ### Registration [#registration] ```tsx import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(ScrollTrigger); ``` ### `toggleActions` [#toggleactions] The `toggleActions` string controls what happens at four scroll events: **onEnter, onLeave, onEnterBack, onLeaveBack**. ```tsx scrollTrigger: { trigger: '.section', start: 'top 80%', // when the top of .section hits 80% of viewport end: 'bottom 20%', // optional end point toggleActions: 'play reverse play reverse' // ↑ ↑ ↑ ↑ // enter leave enterB leaveB } ``` Common `toggleActions` patterns: | Value | Behaviour | | ----------------------------- | ---------------------------------------------- | | `'play none none none'` | Play once, never reverse (default) | | `'play reverse play reverse'` | Play on enter, reverse on leave, bidirectional | | `'play pause resume reset'` | Pause when leaving, resume on re-enter | | `'restart none none reset'` | Restart every time you scroll into view | ### Cards that animate in on scroll [#cards-that-animate-in-on-scroll] ```tsx title="components/gsap-scroll-trigger.tsx" 'use client'; import { useRef, useState } from 'react'; import { useGSAP } from '@gsap/react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(useGSAP, ScrollTrigger); const CARDS = [ { label: 'Design', color: 'from-violet-500 to-purple-600', icon: '🎨' }, { label: 'Build', color: 'from-orange-400 to-red-500', icon: '⚡' }, { label: 'Deploy', color: 'from-emerald-400 to-teal-500', icon: '🚀' } ]; export default function GsapScrollTrigger() { const containerRef = useRef(null); const [replay, setReplay] = useState(0); useGSAP( () => { gsap.from('.scroll-card', { opacity: 0, y: 50, scale: 0.9, duration: 0.5, ease: 'power2.out', stagger: 0.12, scrollTrigger: { trigger: containerRef.current, start: 'top 85%', toggleActions: 'play reverse play reverse' } }); }, { scope: containerRef, dependencies: [replay] } ); return (
{CARDS.map(({ label, color, icon }) => (
{icon} {label}
))}
); } ``` **Live result -scroll into view to trigger, hit ↺ Replay to restart:** *** ## 5 - Replaying Animations [#5---replaying-animations] Every animation in this post has a **↺ Replay** button. The pattern is two lines of code using `useGSAP`'s `dependencies` array. ### How it works [#how-it-works-2] `useGSAP` behaves like `useEffect` -it re-runs whenever its `dependencies` change. When it re-runs, it **automatically kills all tweens and ScrollTriggers** created in the previous run before starting fresh. This gives you a clean replay with zero manual cleanup. ```tsx title="components/gsap-replay.tsx" const [replay, setReplay] = useState(0); useGSAP( () => { // All tweens here are killed and re-created when `replay` changes gsap.from('.element', { opacity: 0, y: 24, duration: 0.5 }); }, { scope: containerRef, dependencies: [replay] } ); // Trigger a replay: ``` ### Why `dependencies: [replay]` instead of `tl.restart()` [#why-dependencies-replay-instead-of-tlrestart] You might think to store a timeline ref and call `tl.restart()` directly: ```tsx // ❌ Fragile -tl.restart() doesn't re-run useGSAP context, // so new DOM nodes or changed props won't be picked up const tlRef = useRef(); useGSAP(() => { tlRef.current = gsap.timeline(); tlRef.current.from('.box', { opacity: 0 }); }, { scope: containerRef }); // This replays the tween but against potentially stale targets ``` The `dependencies` approach is better because: * **Fresh context** -GSAP kills the old context and creates a new one, re-querying all selectors against the current DOM. * **Works with dynamic content** -if the list re-renders with new items, the new elements are picked up. * **Handles ScrollTriggers** -`tl.restart()` doesn't restart nested `ScrollTrigger` instances; the `dependencies` approach does. ### Full example [#full-example] ```tsx title="components/gsap-replay.tsx" 'use client'; import { useRef, useState } from 'react'; import { useGSAP } from '@gsap/react'; import { gsap } from 'gsap'; gsap.registerPlugin(useGSAP); export default function GsapReplay() { const containerRef = useRef(null); const [replay, setReplay] = useState(0); useGSAP( () => { const tl = gsap.timeline({ defaults: { ease: 'power3.out' } }); tl.from('.rp-bar', { scaleX: 0, transformOrigin: 'left', duration: 0.5 }) .from('.rp-icon', { opacity: 0, scale: 0, duration: 0.4 }, '-=0.2') .from('.rp-title', { opacity: 0, x: -16, duration: 0.35 }, '-=0.25') .from('.rp-sub', { opacity: 0, x: -12, duration: 0.3 }, '-=0.2') .from( '.rp-tag', { opacity: 0, y: 8, scale: 0.85, stagger: 0.08, duration: 0.28 }, '-=0.15' ); }, { scope: containerRef, dependencies: [replay] } ); return (
🎬

GSAP Timeline

Sequenced entrance with stagger -all driven by one timeline.

{['gsap', 'timeline', 'stagger'].map((t) => ( {t} ))}
); } ``` **Live result:** *** ## Putting it all together -the `useGSAP` hook [#putting-it-all-together--the-usegsap-hook] `useGSAP` from `@gsap/react` is the idiomatic React way to run GSAP. It is a thin wrapper around `useLayoutEffect` that: 1. Creates a GSAP **context** scoped to your container ref. 2. Automatically **kills all tweens and ScrollTriggers** created inside it when the component unmounts. 3. Works correctly with React 18 Strict Mode double-invocation. ```tsx title="components/hero.tsx" 'use client'; import { useRef } from 'react'; import { useGSAP } from '@gsap/react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(useGSAP, ScrollTrigger); export function Hero() { const containerRef = useRef(null); useGSAP( () => { // All tweens/ScrollTriggers created here are automatically cleaned up const tl = gsap.timeline({ defaults: { ease: 'power3.out', clearProps: 'all' } }); tl.from('.hero-image', { opacity: 0, scale: 0.9, duration: 0.6 }) .from('.hero-heading', { opacity: 0, y: 24, duration: 0.5 }, '-=0.3') .from('.hero-subtext', { opacity: 0, y: 16, duration: 0.4 }, '-=0.2') .from('.hero-cta', { opacity: 0, y: 12, duration: 0.35 }, '-=0.15'); // ScrollTrigger is also auto-killed on unmount gsap.from('.feature-card', { opacity: 0, y: 40, stagger: 0.1, ease: 'power2.out', duration: 0.5, scrollTrigger: { trigger: '.features', start: 'top 80%', toggleActions: 'play reverse play reverse' } }); }, { scope: containerRef } // scopes all class selectors to this element ); return (
hero

Build fast. Ship beautiful.

Animations that feel native, powered by GSAP.

Performance
Flexibility
Control
); } ``` *** ## Best practices [#best-practices] **Always scope with a container ref.** Class-based selectors like `.box` are global. Without `{ scope: containerRef }`, your animation may accidentally target elements in *other* mounted components. **Use `clearProps: 'all'` on entrance tweens.** GSAP leaves inline styles after tweens complete. These override your CSS. `clearProps: 'all'` removes them when the animation finishes. **Register plugins once at module level.** Call `gsap.registerPlugin(...)` outside of components -not inside `useGSAP` or `useEffect`. Calling it inside a component re-registers on every render. **Respect `prefers-reduced-motion`.** Read `useReducedMotion()` from `motion/react` (or `window.matchMedia`) and skip or simplify animations for users who have requested reduced motion. ```tsx import { useReducedMotion } from 'motion/react'; export function AnimatedCard() { const isReduced = useReducedMotion(); const ref = useRef(null); useGSAP(() => { if (isReduced) return; // skip all animation gsap.from(ref.current, { opacity: 0, y: 24, duration: 0.5, clearProps: 'all' }); }, { scope: ref }); return
Content
; } ``` **Kill ScrollTriggers when content changes.** If you re-render a list (e.g. pagination), call `ScrollTrigger.refresh()` or return a cleanup function from `useGSAP` that kills specific instances. *** ## Conclusion [#conclusion] GSAP gives you a level of animation control that CSS and declarative libraries cannot match. The three patterns covered here -**fade-in**, **stagger**, **timeline**, and **ScrollTrigger** -cover the vast majority of production UI animation needs. Start with `gsap.from` + `useGSAP` scoped to a container ref, add a timeline when you need sequencing, and reach for `ScrollTrigger` when you want animations to respond to scroll position. # Caching a Spotify Access Token Properly ## The setup [#the-setup] This site shows what I am listening to. The browser never talks to Spotify. It calls route handlers under `/api/spotify`, and those hold the credentials and call Spotify on its behalf. Spotify's authorization flow gives you two tokens. The **refresh token** is long lived and is the real secret. The **access token** is what you send with each API call, and Spotify's response tells you how long it is good for in an `expires_in` field. When it runs out, you trade the refresh token for a new one. ## The bug [#the-bug] My helper looked roughly like this: ```ts export async function getAccessToken(): Promise { const tokenResponse = await ky .post('https://accounts.spotify.com/api/token', { /* refresh_token grant */ }) .json(); return tokenResponse.access_token; } ``` Every route handler called it first. So every request for "what is playing" was really two requests to Spotify: one to get a token, and one to use it. The now-playing data is polled every 30 seconds, and each of those polls asked for a brand new token, used it once, and threw it away. Nothing was broken. It was only wasteful, and it doubled the surface for rate limits and slow responses. This kind of bug survives for a long time because everything works. ## Step one: remember the token [#step-one-remember-the-token] The simplest fix is a module variable. ```ts title="src/config/spotify.ts" let cachedToken: { value: string; expiresAt: number } | undefined; ``` When a token arrives, store it with the time it stops being valid: ```ts // Spotify issues hour-long tokens. Fall back to that if the field is missing. const lifetimeMs = (tokenResponse.expires_in ?? 3600) * 1000; cachedToken = { value: tokenResponse.access_token, expiresAt: Date.now() + lifetimeMs - EXPIRY_MARGIN_MS }; ``` And check it before asking for another: ```ts if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value; ``` I store the absolute expiry time, not the lifetime. "Valid until this moment" needs one comparison to check. "Valid for this long, starting from some moment I also have to remember" needs two values and is easier to get wrong. ## Step two: do not trust the last minute [#step-two-do-not-trust-the-last-minute] Look at that `- EXPIRY_MARGIN_MS`. ```ts /** Refresh this long before Spotify's stated expiry, so a token handed to a * request cannot lapse while that request is still in flight. */ const EXPIRY_MARGIN_MS = 60_000; ``` Suppose the token expires at `12:00:00` and a request checks the cache at `11:59:59.8`. The token is valid, so it is returned. The request then goes over the network, and Spotify receives it at `12:00:00.3`. Spotify rejects it. The check and the use are not the same moment. There is network time between them, and my clock and Spotify's clock do not agree to the millisecond. So the cache treats the token as expired a minute early. One minute out of an hour costs almost nothing and removes a failure that would otherwise show up rarely, at random, and never on my machine. ## Step three: one refresh at a time [#step-three-one-refresh-at-a-time] There is still a hole. Imagine the token has just expired and three requests arrive together. All three see an expired cache. All three ask Spotify for a new token. Two of those calls are wasted. The fix is to cache the request that is in progress, not only its result: ```ts let pendingToken: Promise | undefined; export async function getAccessToken(): Promise { if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value; pendingToken ??= requestAccessToken().finally(() => { pendingToken = undefined; }); return pendingToken; } ``` The first caller starts the exchange and stores the promise. Anyone who arrives while it is running gets the same promise and waits for the same answer. `??=` only assigns when the variable is empty, so there is never more than one exchange in flight. The `finally` matters as much as the assignment. Without it, a failed exchange would leave a rejected promise in `pendingToken` for good, and every later call would fail instantly without ever trying again. Clearing it when the promise settles, whether it succeeded or not, means the next caller gets a fresh attempt. JavaScript makes this safe without locks. There is no `await` between checking `pendingToken` and assigning it, so two callers cannot both see it empty. ## Where this stops helping [#where-this-stops-helping] It is worth being honest about what a module variable is. On a serverless platform, each running instance of a function has its own memory. The cache is per instance. A new instance starts empty and does one exchange of its own. If the platform runs several instances at once, each one holds its own token. And when an instance is shut down, its cache goes with it. So this does not make it "one token exchange per hour". It makes it "one exchange per instance per hour, at most". For a personal site, that is a big improvement over one per request, and it needs no extra infrastructure. If I needed a truly shared token, the next step would be an external store such as Redis, with the same three ideas: store the expiry, keep a margin, and make sure only one caller refreshes. I do not need that here, so I have not built it. ## One rule I did not break [#one-rule-i-did-not-break] It is tempting to solve this from the other side: send the access token to the browser and let the browser call Spotify directly. That would remove the proxying altogether. It would also hand every visitor a working credential for my Spotify account. An endpoint that returns a live access token is a leak, even if the token is short lived. The token stays on the server. The browser only ever gets the trimmed data it needs to draw a song title. ## Summary [#summary] * Cache the token with its absolute expiry time. * Expire it early, because checking and using are different moments. * Share the in-flight promise so a burst of requests triggers one refresh. * Clear that promise in `finally`, so a failure does not stick. * Remember that module memory is per instance, and decide whether that is enough. # Difference between createRef, useRef and forwardRef Refs are in react are used to create references to certain values and nodes in the react DOM. Refs in React provide a way to access DOM nodes or React elements created in the render method. While they might seem similar at first glance, `createRef`, `useRef`, and `forwardRef` serve distinct purposes and are used in different scenarios. ## Comparison Table [#comparison-table] | Feature | `createRef` | `useRef` | `forwardRef` | | --------------- | --------------------------------- | ----------------------------------------------------- | -------------------------------------------------------- | | **Type** | Function (for class components) | Hook (for functional components) | HOC (Higher-Order Component) | | **Purpose** | Create a ref for class components | Create a mutable ref object for functional components | Pass a ref from a parent to a child functional component | | **Persistence** | New ref on every render | Persists across renders | Parent's ref is attached to a child's DOM node | | **Usage** | `this.myRef = createRef();` | `const myRef = useRef(initialValue);` | `const MyComponent = forwardRef((props, ref) => ...)` | | **Mutating** | `this.myRef.current` | `myRef.current` | `ref` is passed and attached to a DOM node | | **When to Use** | Legacy class components | Functional components and stable references | Parent needs direct access to child DOM node | ## createRef (Functional Equivalent with useRef) [#createref-functional-equivalent-with-useref] Although `createRef` is traditionally used in class components, its functional counterpart is `useRef`, which serves the same purpose in functional components. ### Example: using `useRef` for DOM access [#example-using-useref-for-dom-access] ```tsx title="App.ts" import React, { useRef } from 'react'; export function CreateRefFunctionalExample() { const myInputRef = useRef(null); const focusInput = () => { if (myInputRef.current) { myInputRef.current.focus(); } }; return (

`createRef` Equivalent with `useRef` (Functional)

); } ``` ## useRef [#useref] useRef is a Hook that returns a mutable ref object persisting across renders. It's the go-to ref tool for functional components. ### Example 1: Accessing a DOM Element [#example-1-accessing-a-dom-element] ```tsx import React, { useRef } from 'react'; export function ComponentWithRef() { const myInputRef = useRef(null); const focusInput = () => { if (myInputRef.current) { myInputRef.current.focus(); } }; return (

`useRef` (Accessing DOM Element)

); } ``` ### Example 2: Storing Mutable Value [#example-2-storing-mutable-value] ```tsx import React, { useRef } from 'react'; export function CounterWithoutRerender() { const countRef = useRef(0); const increment = () => { countRef.current++; console.log('Count (from ref):', countRef.current); }; return (

`useRef` (Storing Mutable Value)

Count (from ref - check console): {countRef.current}

(Note: The displayed count above won't update until a re-render is triggered by something else.)

); } ``` ## forwardRef [#forwardref] forwardRef is a Higher-Order Component that allows a parent component to pass a ref down to a child, which can attach it to a DOM node. 💡 Without forwardRef, passing a ref to a functional component will result in an error. ### Example: using forwardRef on components [#example-using-forwardref-on-components] ```tsx twoslash title="button.tsx" import React, { forwardRef } from 'react'; interface ButtonProps extends React.ButtonHTMLAttributes { className?: string; } const Button = forwardRef(({ children, className, ...props }, ref) => { return ( ); }); Button.displayName = 'Button'; export { Button }; ``` Using the button Component ```tsx title="App.tsx" import React, { useRef } from 'react'; import { Button } from './button'; export default function AppWithForwardRef() { const buttonRef = useRef(null); const handleFocusButtonClick = () => { if (buttonRef.current) { buttonRef.current.focus(); } }; return (

`forwardRef` Example

); } ``` ## Conclusion [#conclusion] | `USE THIS` | `WHEN` | | --------------------------------------------------- | ---------------------------------------------------------------------------- | | `createRef` (via `useRef` in functional components) | For accessing DOM or component refs in functional components. | | `useRef` | For mutable values or DOM references that persist across renders. | | `forwardRef` | When a parent needs access to a DOM element in a child functional component. | # Difference between WHILE and DO WHILE loop Loops are essential constructs in programming, allowing developers to execute a block of code repeatedly based on a condition. Two commonly used types of loops are the while loop and the do-while loop. Although they serve similar purposes, there are subtle differences between them that are crucial to understand. In this article, we’ll explore the differences between while and do-while loops in Typescript. ## The While loop [#the-while-loop] The while loop is a pre-test loop, meaning it evaluates the condition before executing the loop's body. If the condition is false initially, the loop body will never execute. The syntax for a while loop is as follows: ```ts title="while-loop.ts" while (condition) { // loop body } ``` ### Example [#example] The following example demonstrates a while loop that prints to the console the numbers from 1 to 5 using a counter variable: ```ts title="while-loop.ts" let i: number = 1; while (i <= 5) { console.log(i); i++; } ``` ## The Do-While loop [#the-do-while-loop] The do-while loop is a post-test loop, meaning it evaluates the condition after executing the loop's body. This guarantees that the loop body will execute at least once, even if the condition is false initially. The syntax for a do-while loop is as follows: ```ts title="do-while-loop.ts" do { // loop body } while (condition); ``` ### Example [#example-1] The following example demonstrates a do-while loop that prints to the console the numbers from 1 to 5 using a counter variable: ```ts title="do-while-loop.ts" twoslash let i: number = 1; do { console.log(i); i++; } while (i <= 5); ``` ## Key Differences [#key-differences] ### Execution Guarantee [#execution-guarantee] * `while` loop: The loop body will execute only if the condition is true initially. * `do-while` loop: The loop body will execute at least once, even if the condition is false initially. ### Pre-test vs. Post-test [#pre-test-vs-post-test] * `while` loop: The condition is evaluated before executing the loop body. * `do-while` loop: The condition is evaluated after executing the loop body. ## Conclusion [#conclusion] Understanding the differences between while and do-while loops is fundamental for writing efficient and error-free code. Whether you choose a while loop for its simplicity and straightforwardness or a do-while loop for its guaranteed initial execution, selecting the right type of loop depends on the specific requirements of your program. By mastering these looping constructs, you enhance your ability to create robust and effective algorithms in your programs. # Fetch API vs Axios for HTTP requests ## Fetch API [#fetch-api] The Fetch API provides a JavaScript interface for accessing and manipulating parts of the HTTP pipeline, such as requests and responses. It also provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across a network. ### Syntax [#syntax] ```ts language="ts" title="fetch.ts" fetch(input, init); ``` ### Parameters [#parameters] * **input** - The URL you wish to fetch. * **init** - An object with any custom settings that you want to apply to the request. The possible options are: * **method** - The request method, e.g., `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. The default is `GET`. * **headers** - Any headers you want to add to your request, contained within a Headers object or an object literal with ByteString values. * **body** - Any body that you want to add to your request: this can be a `Blob`, `BufferSource`, `FormData`, `URLSearchParams`, or `USVString` object. Note that a request using the GET or HEAD method cannot have a body. * **mode** - The mode you want to use for the request, e.g., `cors`, `no-cors`, `same-origin`, or `navigate`. The default is `cors` which stands for `Cross-Origin`. * **credentials** - The request credentials you want to use for the request: `omit`, `same-origin`, or `include`. The default is `same-origin`. * **cache** - The cache mode you want to use for the request: `default`, `no-store`, `reload`, `no-cache`, `force-cache`, or `only-if-cached`. * **redirect** - The redirect mode to use: `follow`, `error`, or `manual`. The default is `follow`. * **referrer** - A USVString specifying `no-referrer`, `client`, or a URL. The default is `client`. * **referrerPolicy** - Specifies the value of the referer HTTP header. May be one of `no-referrer`, `no-referrer-when-downgrade`, `origin`, `origin-when-cross-origin`, `same-origin`, `strict-origin`, `strict-origin-when-cross-origin`, or `unsafe-url`. * **integrity** - Contains the subresource integrity value of the request (e.g., sha256-BpfBw7ivV8q2jLiT13fxDYAeHtYK7DDiZ0L+FL/4vow=). ### Example [#example] #### TRY-CATCH METHOD [#try-catch-method] The try catch method is an efficient way to handle errors in code. Using the fetch API with a `try-catch` block is a good way to implement error handling with the `fetch` API because more can be done and it can ensure clean and a readable code. Using the `{JSON} Placeholder` API, we can fetch data from the API and handle errors with the `try-catch` method. ```ts title="try-catch-method.ts" twoslash const token = 'my-token'; type ErrorCause = Error & { cause?: { error: Error; response: Response }; }; try { // Fetch data from the API const response = await fetch('https://jsonplaceholder.typicode.com/todos/1', { method: 'GET', // method can be GET, POST, PUT, PATCH, DELETE depending on the API endpoint definition mode: 'cors', // mode to use for request headers: { Authorization: `Bearer ${token}`, // Add token to the header if needed for authentication and authorization 'Content-Type': 'application/json' // Add content type if needed } }); // Check if the response is ok and throw an error to the catch block if not if (!response.ok) throw new Error('Something went wrong', { cause: { response } }); // perform some action with the response // make toast notification // Parse the response to JSON const data = await response.json(); // do something with the data } catch (error) { let err: ErrorCause; if (error instanceof Error) err = error as ErrorCause; else err = new Error('Error fetching todo', { cause: { error } }) as ErrorCause; // since the error was thrown from the try block with the response object, we can access the response object from the error object // Handle errors based on the status code switch (err.cause?.response?.status) { case 400: // bad request - invalid data // perform some action break; case 401: // unauthorized - invalid token // perform some action break; // others default: // perform some action break; } } ``` #### USING THE PROMISES-CHAIN METHOD [#using-the-promises-chain-method] The promises-chain method is another way to use the fetch API. This method is not as efficient as the `try-catch` method because it is not as readable and might not be as clean as the `try-catch` method. ```ts title="promises-chain-method.ts" twoslash const token = 'my-token'; type ErrorCause = Error & { cause?: { error: Error; response: Response }; }; // Fetch data from the API fetch('https://jsonplaceholder.typicode.com/todos/1', { method: 'GET', // method can be GET, POST, PUT, PATCH, DELETE depending on the API endpoint definition mode: 'cors', // mode to use for request headers: { Authorization: `Bearer ${token}`, // Add token to the header if needed for authentication and authorization 'Content-Type': 'application/json' // Add content type if needed } }) .then((response) => { // Check if the response is ok and throw an error to the catch block if not if (!response.ok) throw new Error('Something went wrong', { cause: { response } }); // perform some action with the response // make toast notification // Parse the response to JSON return response.json(); }) .then((data) => { // do something with the data }) .catch((error) => { let err: ErrorCause; if (error instanceof Error) err = error as ErrorCause; else err = new Error('Error fetching todo', { cause: { error } }) as ErrorCause; // handle }); ``` ## Axios [#axios] Axios is a promise-based HTTP client for the browser and Node.js. It has a convenient and modern API simplifying asynchronous HTTP request and response handling. ### Syntax [#syntax-1] ```ts language="ts" title="axios.ts" axios(url, config); ``` # Grid Card animation with framer motion ## Requirements [#requirements] A simple layout animation with framer motion and react. Animating the active card in a grid of cards can help the user to focus on the content of the card. This is a simple example of how to do it. This post assumes that you have a basic understanding of React. If you are new to React, you can follow the [React getting started guide](https://react.dev/). We will also use [tailwindcss](https://tailwindcss.com/) for styling and [motion/react](https://www.framer.com/motion/) a popular animation library for React. ## Setup [#setup] ### Dependencies Installation [#dependencies-installation] We will install the required dependencies for the project. npm pnpm yarn bun ```bash npm install motion/react tailwindcss postcss autoprefixer clsx ``` ```bash pnpm add motion/react tailwindcss postcss autoprefixer clsx ``` ```bash yarn add motion/react tailwindcss postcss autoprefixer clsx ``` ```bash bun add motion/react tailwindcss postcss autoprefixer clsx ``` ### Styling the Grid Card [#styling-the-grid-card] We will use tailwindcss to style the tabs. We will create a new file `tailwind.config.js` in the root of the project and add the following code. ```js title="tailwind.config.js" module.exports = { experimental: { optimizeUniversalDefaults: true }, content: ['./src/**/*.{js,jsx,ts,tsx,vue,mdx,md}'], darkMode: 'class', theme: { extend: {} }, plugins: [] }; ``` We will also create a new file `postcss.config.js` in the root of the project and add the following code. ```js title="postcss.config.js" module.exports = { plugins: { 'tailwindcss/nesting': {}, // enable css nesting tailwindcss: {}, autoprefixer: {} } }; ``` We will also update the `tailwind.css` file in the `styles` folder with the following code. ```css title="styles/tailwind.css" @tailwind base; @tailwind components; @tailwind utilities; ``` ## Creating the Component [#creating-the-component] We will create a new file `animated-grid.tsx` in the `components` folder and add the following code. ```tsx title="components/animated-grid.tsx" 'use client'; import { useState } from 'react'; import Image from 'next/image'; import { AnimatePresence, motion } from 'motion/react'; import { HiX } from 'react-icons/hi'; interface ProductI { id: string; name: string; price: number; image: { url: string; }; type: string; weight: number; description: string; createdAt: string; updatedAt: string; } const products: ProductI[] = [ { id: 'ecdc8405-87a9-4950-872e-885e27311481', name: 'Battlefield', price: 88.99, type: 'book', weight: 0.07, description: 'Game, Thriller, Action', image: { url: '/images/grid-component/540202.jpg' }, createdAt: '2023-04-30T18:54:35.564Z', updatedAt: '2023-04-30T18:54:35.564Z' }, { id: 'e8fb7251-6d54-46f0-995f-28845fc6cf30', name: "Assasin's Creed", price: 87.99, type: 'book', weight: 0.08, description: 'Thriller, Game, Action', image: { url: '/images/grid-component/320623.png' }, createdAt: '2023-04-30T18:55:30.687Z', updatedAt: '2023-04-30T18:55:30.687Z' } ]; export default function AnimatedGridComponent() { const [selectedProduct, setSelectedProduct] = useState(null); return (
{/* body */}
{products.map((product) => ( {/* body */}
{product.id}
{product.name}
$ {product.price}
Weight : {product.weight} KG
{/* actions */}
))}
{selectedProduct && ( setSelectedProduct(null)} />
{selectedProduct.name}
{/* about */}
{selectedProduct.id}
{selectedProduct.name}
$ {selectedProduct.price}
Weight : {selectedProduct.weight} KG
{/* type */}
{selectedProduct.type}
)}
); } ``` ## Conclusion [#conclusion] We have successfully created a grid with a selected item animation. You can use this animation to create a grid of products, images, or any other content. You can also use this animation to create a grid of cards with a selected item animation. Note this uses tailwindcss v3. # A Simple Guide to Context in Golang ## What is Context? [#what-is-context] Context is a package in Golang that allows you to pass data, cancel a process and more. It is a powerful feature that can be used in many different ways and built into the go standard library. Go's `context` package is one of those fundamental pieces of the language that, once you "get it," unlocks a whole new level of control and robustness in your concurrent applications. Let's dive in. ## Why Do We Need Context? [#why-do-we-need-context] In Go, when you launch goroutines, they run independently. If you start a long-running operation in a goroutine (like a database query, an external API call, or a complex computation), how do you tell it to stop if the client disconnects, the request times out, or a parent operation is canceled? Without `context`, this can become a tangled mess of channels and manual signaling. `context` provides a standardized, idiomatic way to manage these concerns. It allows you to propagate cancellation signals, deadlines, and request-scoped values down the call chain, across goroutine boundaries. ## The `Context` Interface [#the-context-interface] At its core, `context.Context` is an interface: ```go title="context.go" // From the Go standard library: type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any } ``` * `Deadline()`: Returns the time when the context will be canceled (if a deadline is set). * `Done()`: Returns a channel that is closed when the context is canceled or its deadline passes. This is what goroutines listen to for cancellation signals. * `Err()`: Returns the error that caused the context to be canceled. * `Value(key any)`: Retrieves a value associated with a given key from the context. ## Root Contexts [#root-contexts] You'll usually start with one of two "root" contexts: ### `context.Background()` [#contextbackground] This is the ultimate ancestor of all contexts. It's a non-nil, empty context that is never canceled. You use it as the base for the top-level operations in your application, like a main function or an incoming HTTP request. ```go title="example/background.go" showLineNumbers> package main import ( "context" "fmt" "time" ) func main() { ctx := context.Background() fmt.Println("Background Context:", ctx) // You wouldn't typically use Background directly for long-running ops // but as a starting point to derive other contexts. time.Sleep(1 * time.Second) } ``` ### `context.TODO()` [#contexttodo] Similar to Background(), it's an empty context that's never canceled. However, TODO() is used when you're unsure which context to use, or if the parent context isn't yet available (e.g., in early stages of development or when prototyping). It's a placeholder, a signal that you need to fill in the proper context later. Avoid using `context.TODO()` in production code. It's meant for temporary use and indicates incomplete design. ## Derived Contexts: Adding Functionality [#derived-contexts-adding-functionality] Most of the time, you'll derive new contexts from an existing one (often Background()). This creates a tree-like hierarchy, where canceling a parent context automatically cancels all its children. ### `context.WithCancel()` [#contextwithcancel] This is your go-to for explicit cancellation. It returns a new context and a CancelFunc. Calling the CancelFunc signals all goroutines listening to that context's Done() channel to stop. ```go title="example/with-cancel.go" package main import ( "context" "fmt" "time" ) func performTask(ctx context.Context, taskName string) { fmt.Printf("%s: Starting...\n", taskName) select { case <-time.After(3 * time.Second): // Simulate work taking 3 seconds fmt.Printf("%s: Completed!\n", taskName) case <-ctx.Done(): // Context was canceled fmt.Printf("%s: Canceled! Reason: %v\n", taskName, ctx.Err()) } } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Always call cancel to release resources go performTask(ctx, "Long Running Task") // Simulate some other work time.Sleep(1 * time.Second) fmt.Println("Main: Canceling the task after 1 second...") cancel() // Cancel the context time.Sleep(2 * time.Second) // Give time for the goroutine to react fmt.Println("Main: Exiting.") } ``` #### Output: [#output] ```bash Long Running Task: Starting... Main: Canceling the task after 1 second... Long Running Task: Canceled! Reason: context canceled Main: Exiting. ``` ### `context.WithTimeout()` [#contextwithtimeout] For operations that must complete within a certain duration, WithTimeout() is invaluable. It returns a new context and a CancelFunc. The context automatically cancels itself after the specified timeout duration. ```go title="example/timeout.go" package main import ( "context" "fmt" "time" ) func fetchData(ctx context.Context) { fmt.Println("Fetching data: Starting...") select { case <-time.After(2 * time.Second): // Simulate network call taking 2 seconds fmt.Println("Fetching data: Successfully retrieved!") case <-ctx.Done(): // Context timed out fmt.Printf("Fetching data: Timed out! Reason: %v\n", ctx.Err()) } } func main() { // Set a timeout of 1 second ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() // Don't forget to call cancel! go fetchData(ctx) time.Sleep(3 * time.Second) // Give time for goroutine to finish or timeout fmt.Println("Main: Exiting.") } ``` #### Output [#output-1] ```bash Fetching data: Starting... Fetching data: Timed out! Reason: context deadline exceeded Main: Exiting. ``` ### `context.WithDeadline()` [#contextwithdeadline] Similar to `context.WithTimeout()`, but instead of a duration, you specify an exact time.Time when the context should be canceled. ```go title="example/deadline.go" package main import ( "context" "time" ) func main() { // Example usage (conceptual): deadline := time.Now().Add(5 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() // ... use ctx } ``` ### `context.WithValue()` [#contextwithvalue] This allows you to attach request-scoped values to a context. This is useful for passing metadata like a request ID, user ID, or tracing information down the call chain without polluting function signatures. #### Important Considerations for `context.WithValue` [#important-considerations-for-contextwithvalue] * Keys should be custom types: Use `type myKey` string or an unexported struct type as a key to avoid collisions with other packages. * Values should be immutable: Contexts are designed for read-only values. * Not a replacement for arguments: Don't use `context.WithValue` to pass essential function parameters; pass them explicitly. * Use sparingly: Overuse can lead to less explicit dependencies. ```go title="example/with-value.go" package main import ( "context" "fmt" "time" ) // Define a custom type for the key to avoid collisions type RequestIDKey string func processRequest(ctx context.Context) { requestID := ctx.Value(RequestIDKey("requestID")) if requestID != nil { fmt.Printf("Processing request with ID: %s\n", requestID) } else { fmt.Println("Processing request (no ID found)") } select { case <-time.After(1 * time.Second): fmt.Println("Request processed successfully.") case <-ctx.Done(): fmt.Printf("Request processing canceled. Reason: %v\n", ctx.Err()) } } func main() { // Create a context with a request ID ctx := context.WithValue(context.Background(), RequestIDKey("requestID"), "req-12345") // Start processing the request in a goroutine go processRequest(ctx) // Simulate main function doing other things time.Sleep(2 * time.Second) fmt.Println("Main: Exiting.") } ``` #### Output [#output-2] ```bash Processing request with ID: req-12345 Request processed successfully. Main: Exiting. ``` ## Widely Adopted Practices [#widely-adopted-practices] 1. **_Pass Context as the First Argument_** Always pass context.Context as the very first argument in your function signatures. This is a widely adopted Go convention: ```go func MyFunction(ctx context.Context, arg1 string, arg2 int) error { // ... } ``` 2. **_Always defer `cancel()`_** When you use `context.WithCancel`, `context.WithTimeout`, or `context.WithDeadline`, you receive a CancelFunc. Always defer calling this function to ensure resources associated with the context are released, even if the operation completes or panics. 3. **_Error Handling_** Use `ctx.Err()` to determine why a context was canceled (e.g., `context.Canceled` or `context.DeadlineExceeded`). ## Conclusion [#conclusion] The context package is a cornerstone of modern Go programming, especially when dealing with concurrency, network services, and long-running operations. By providing a standardized way to manage cancellation, deadlines, and request-scoped values, it helps you write more robust, efficient, and well-behaved Go applications. Master context, and you'll wield a powerful tool for building production-ready Go services. # A Simple Guide to Context in React Context is a powerful feature in React that allows you to share state between components without having to pass props down manually at every level. `Prop Drilling` is a term used to describe the process of passing props from a parent component to a child component, then to another child component, and so on. This can be a tedious process, especially if you have a deeply nested component tree. Context solves this problem by allowing you to pass data through the component tree without having to pass props down manually at every level. **NOTE:** Context is not a replacement for Redux or other state management libraries. It is meant to be used for sharing state between components that are closely related, such as siblings or cousins. This a post assumes you have a basic understanding of React and JavaScript. ## Creating a Context (React) [#creating-a-context-react] The first step in using Context is to create a context object. This is done using the `createContext` method from the React library. The `createContext` method takes an initial value as an argument and returns a context object. The initial value is used when no provider is found in the component tree. This is useful for testing purposes or when you want to provide a default value for your context. ```tsx twoslash import * as React from 'react'; const MyContext = React.createContext('default value'); ``` # A Simple Guide to Error Handling in Go Error handling in Go is different from many other programming languages. Instead of using try-catch blocks or exceptions, Go treats errors as values that you explicitly check and handle. While this might seem verbose at first, it leads to more robust and predictable code. **NOTE:** This post assumes you have a basic understanding of Go and programming concepts. Go's approach to error handling emphasizes clarity and explicit handling over hidden control flow. ## The Error Interface [#the-error-interface] In Go, an error is any value that implements the following interface: ```go title="error-interface.go"type error interface { Error() string } ``` This simple interface is all you need to create and work with errors in Go. ## Basic Error Handling [#basic-error-handling] The most common pattern in Go is to return an error as the last return value from a function: ```go title="basic-error-handling.go"package main import ( "errors" "fmt" ) func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } func main() { result, err := divide(10, 0) if err != nil { fmt.Println("Error:", err) return } fmt.Println("Result:", result) } ``` ## Creating Errors [#creating-errors] ### Using errors.New() [#using-errorsnew] The simplest way to create an error: ```go title="errors-new.go"package main import ( "errors" "fmt" ) func greet(name string) (string, error) { if name == "" { return "", errors.New("name cannot be empty") } return "Hello, " + name, nil } func main() { msg, err := greet("") if err != nil { fmt.Println("Error:", err) return } fmt.Println(msg) } ``` ### Using fmt.Errorf() [#using-fmterrorf] For more detailed error messages with formatting: ```go title="fmt-errorf.go"package main import ( "fmt" ) func validateAge(age int) error { if age < 0 { return fmt.Errorf("age cannot be negative: %d", age) } if age > 120 { return fmt.Errorf("age seems unrealistic: %d", age) } return nil } func main() { err := validateAge(-5) if err != nil { fmt.Println("Validation error:", err) } } ``` ## Wrapping Errors (Go 1.13+) [#wrapping-errors-go-113] Go 1.13 introduced error wrapping, which allows you to add context while preserving the original error: ```go title="error-wrapping.go"package main import ( "errors" "fmt" ) func readFile(filename string) ([]byte, error) { return nil, errors.New("file not found") } func processFile(filename string) error { data, err := readFile(filename) if err != nil { // Wrap the error with additional context return fmt.Errorf("processFile failed: %w", err) } fmt.Println("File data:", data) return nil } func main() { err := processFile("data.txt") if err != nil { fmt.Println("Error:", err) // Check if a specific error is in the chain if errors.Is(err, errors.New("file not found")) { fmt.Println("The file was not found") } } } ``` ### Unwrapping Errors [#unwrapping-errors] Use `errors.Unwrap()` to get the underlying error: ```go title="error-unwrap.go"package main import ( "errors" "fmt" ) func main() { baseErr := errors.New("base error") wrappedErr := fmt.Errorf("wrapped: %w", baseErr) // Unwrap to get the original error unwrapped := errors.Unwrap(wrappedErr) fmt.Println("Unwrapped:", unwrapped) // Check if errors are the same fmt.Println("Is same:", errors.Is(wrappedErr, baseErr)) } ``` ## Custom Error Types [#custom-error-types] You can create your own error types by implementing the `error` interface: ```go title="custom-error-type.go"package main import ( "fmt" ) // ValidationError represents a validation error type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed for field '%s': %s", e.Field, e.Message) } func validateEmail(email string) error { if email == "" { return &ValidationError{ Field: "email", Message: "cannot be empty", } } return nil } func main() { err := validateEmail("") if err != nil { fmt.Println("Error:", err) // Type assertion to access custom error properties if validationErr, ok := err.(*ValidationError); ok { fmt.Printf("Field: %s, Message: %s\n", validationErr.Field, validationErr.Message) } } } ``` ## Errors with Multiple Return Values [#errors-with-multiple-return-values] It's common in Go to return both a result and an error: ```go title="multiple-return.go"package main import ( "fmt" ) func getUser(id int) (string, error) { if id <= 0 { return "", fmt.Errorf("invalid user ID: %d", id) } users := map[int]string{ 1: "Alice", 2: "Bob", } name, exists := users[id] if !exists { return "", fmt.Errorf("user not found: %d", id) } return name, nil } func main() { name, err := getUser(1) if err != nil { fmt.Println("Error:", err) return } fmt.Println("User:", name) } ``` ## Panic and Recover [#panic-and-recover] Go has a mechanism called `panic` and `recover` for exceptional situations. Think of panic like exceptions in other languages, but use them sparingly. ### Panic [#panic] Panic is used when a program cannot continue: ```go title="panic.go"package main import "fmt" func process(data []int) { if len(data) == 0 { panic("cannot process empty data") } fmt.Println("Processing:", data) } func main() { data := []int{} process(data) } ``` ### Recover [#recover] Recover is only useful inside deferred functions: ```go title="recover.go"package main import ( "fmt" ) func safeProcess(data []int) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %v", r) } }() if len(data) == 0 { panic("cannot process empty data") } return nil } func main() { err := safeProcess([]int{}) if err != nil { fmt.Println("Handled error:", err) } } ``` **Warning:** Panic and recover should only be used for truly exceptional situations, not for normal error handling. ## Error Handling Patterns [#error-handling-patterns] ### Sentinel Errors [#sentinel-errors] Sentinel errors are predefined errors that you can check for specifically: ```go title="sentinel-errors.go"package main import ( "errors" "fmt" ) var ( ErrUserNotFound = errors.New("user not found") ErrInvalidInput = errors.New("invalid input") ) func getUser(id int) (string, error) { if id <= 0 { return "", ErrInvalidInput } if id > 100 { return "", ErrUserNotFound } return "User", nil } func main() { _, err := getUser(101) if err != nil { if errors.Is(err, ErrUserNotFound) { fmt.Println("User does not exist") } else if errors.Is(err, ErrInvalidInput) { fmt.Println("Invalid user ID") } else { fmt.Println("Other error:", err) } } } ``` ### Error Types with Methods [#error-types-with-methods] You can add methods to your custom error types for more sophisticated error handling: ```go title="error-with-methods.go"package main import ( "fmt" "net/http" ) // HTTPError represents an HTTP error type HTTPError struct { StatusCode int Message string } func (e *HTTPError) Error() string { return fmt.Sprintf("HTTP error %d: %s", e.StatusCode, e.Message) } // Temporary returns true if the error is temporary func (e *HTTPError) Temporary() bool { return e.StatusCode >= 500 || e.StatusCode == http.StatusTooManyRequests } func fetchData(url string) error { return &HTTPError{ StatusCode: http.StatusTooManyRequests, Message: "rate limit exceeded", } } func main() { err := fetchData("https://api.example.com") if err != nil { if httpErr, ok := err.(*HTTPError); ok { fmt.Printf("Status: %d\n", httpErr.StatusCode) if httpErr.Temporary() { fmt.Println("This is a temporary error, retry later") } } } } ``` ### Aggregate Errors [#aggregate-errors] Sometimes you need to collect multiple errors: ```go title="aggregate-errors.go"package main import ( "fmt" "strings" ) // MultiError collects multiple errors type MultiError []error func (e MultiError) Error() string { var errs []string for _, err := range e { errs = append(errs, err.Error()) } return strings.Join(errs, "; ") } // Add appends an error to the MultiError func (e *MultiError) Add(err error) { if err != nil { *e = append(*e, err) } } // ToError returns the MultiError as an error, or nil if empty func (e MultiError) ToError() error { if len(e) == 0 { return nil } return e } func main() { var multiErr MultiError multiErr.Add(fmt.Errorf("error 1")) multiErr.Add(fmt.Errorf("error 2")) if err := multiErr.ToError(); err != nil { fmt.Println("Errors:", err) } } ``` In Go 1.20+, you can use `errors.Join()` for this: ```go title="errors-join.go"package main import ( "errors" "fmt" ) func main() { err1 := fmt.Errorf("error 1") err2 := fmt.Errorf("error 2") // Join multiple errors combined := errors.Join(err1, err2) fmt.Println("Combined error:", combined) // Check if specific error is in the chain fmt.Println("Contains err1:", errors.Is(combined, err1)) } ``` ## Best Practices [#best-practices] ### 1. Always Handle Errors [#1-always-handle-errors] Never ignore errors: ```go title="bad-error-handling.go"// BAD: Ignoring error data, _ := readFile("data.txt") // GOOD: Handle the error data, err := readFile("data.txt") if err != nil { return fmt.Errorf("failed to read file: %w", err) } ``` ### 2. Add Context When Wrapping [#2-add-context-when-wrapping] Add helpful context when wrapping errors: ```go title="error-context.go"// BAD: No context if err != nil { return err } // GOOD: Added context if err != nil { return fmt.Errorf("failed to load user: %w", err) } ``` ### 3. Return Early for Errors [#3-return-early-for-errors] The common Go pattern is to check errors and return early: ```go title="early-return.go"func processData(id int) error { // Validate input if id <= 0 { return fmt.Errorf("invalid ID: %d", id) } // Fetch data data, err := fetchData(id) if err != nil { return fmt.Errorf("fetch failed: %w", err) } // Process data err = analyze(data) if err != nil { return fmt.Errorf("analysis failed: %w", err) } return nil } ``` ### 4. Use Descriptive Error Messages [#4-use-descriptive-error-messages] Make error messages helpful: ```go title="descriptive-errors.go"// BAD: Vague error return errors.New("error occurred") // GOOD: Descriptive error return fmt.Errorf("failed to connect to database '%s' after 3 attempts", dbHost) ``` ### 5. Don't Use Panics for Normal Errors [#5-dont-use-panics-for-normal-errors] Panics should be reserved for truly exceptional situations: ```go title="panic-usage.go"// BAD: Using panic for normal error handling func divide(a, b int) int { if b == 0 { panic("division by zero") } return a / b } // GOOD: Returning an error func divide(a, b int) (int, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } ``` ## Real-World Examples [#real-world-examples] ### File Operations [#file-operations] ```go title="file-operations.go"package main import ( "fmt" "os" ) func processFile(filename string) error { // Open file file, err := os.Open(filename) if err != nil { return fmt.Errorf("failed to open file '%s': %w", filename, err) } defer file.Close() // Read file data := make([]byte, 1024) n, err := file.Read(data) if err != nil { return fmt.Errorf("failed to read from file '%s': %w", filename, err) } fmt.Printf("Read %d bytes: %s\n", n, string(data[:n])) return nil } func main() { err := processFile("example.txt") if err != nil { fmt.Printf("Error: %v\n", err) } } ``` ### HTTP Server [#http-server] ```go title="http-server-errors.go"package main import ( "encoding/json" "fmt" "net/http" ) type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` } var ( ErrUserNotFound = fmt.Errorf("user not found") ErrInvalidJSON = fmt.Errorf("invalid JSON") ErrInvalidUserData = fmt.Errorf("invalid user data") ) func getUserHandler(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") if id == "" { http.Error(w, "missing id parameter", http.StatusBadRequest) return } user, err := fetchUser(id) if err != nil { if err == ErrUserNotFound { http.Error(w, err.Error(), http.StatusNotFound) } else { http.Error(w, "internal server error", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } func fetchUser(id string) (*User, error) { // Simulate database lookup if id != "1" { return nil, ErrUserNotFound } return &User{ID: 1, Name: "Alice", Email: "alice@example.com"}, nil } func main() { http.HandleFunc("/user", getUserHandler) fmt.Println("Server listening on :8080") http.ListenAndServe(":8080", nil) } ``` ### Database Operations [#database-operations] ```go title="database-errors.go"package main import ( "errors" "fmt" ) var ( ErrNoRows = errors.New("no rows in result set") ErrDuplicateKey = errors.New("duplicate key") ErrConnection = errors.New("connection error") ) type DB struct { // Database connection } func (db *DB) GetUser(id int) (*User, error) { // Simulate database query if id == 0 { return nil, fmt.Errorf("get user failed: %w", ErrNoRows) } return &User{ID: id, Name: "Alice"}, nil } func (db *DB) CreateUser(user *User) error { // Simulate duplicate key check if user.ID == 1 { return fmt.Errorf("create user failed: %w", ErrDuplicateKey) } return nil } func main() { db := &DB{} // Handle user lookup user, err := db.GetUser(0) if err != nil { if errors.Is(err, ErrNoRows) { fmt.Println("User not found") } else { fmt.Printf("Database error: %v\n", err) } } else { fmt.Printf("Found user: %v\n", user) } } type User struct { ID int Name string } ``` ## Conclusion [#conclusion] Error handling in Go is explicit, predictable, and encourages careful consideration of error cases. By treating errors as values, Go makes it clear when errors need to be handled and encourages developers to think about failure modes. Key takeaways: * **Check errors explicitly** - Never ignore returned errors * **Add context** - Wrap errors with additional context using `fmt.Errorf` with `%w` * **Use custom error types** - For domain-specific error handling * **Return errors early** - The common Go pattern for clean code * **Reserve panic for truly exceptional cases** - Not for normal error handling * **Make errors helpful** - Provide clear, actionable error messages By following these patterns and best practices, you'll write more robust and maintainable Go code that handles errors gracefully. # A Simple Guide to Goroutines and Channels in Go Concurrency is one of Go's standout features, and goroutines with channels are the tools that make concurrent programming in Go both powerful and approachable. Unlike threads in many other languages, goroutines are lightweight, managed by the Go runtime, and make it easy to run functions concurrently. **NOTE:** This post assumes you have a basic understanding of Go and programming concepts. We'll explore both goroutines and channels, and see how they work together to build concurrent programs. ## What Are Goroutines? [#what-are-goroutines] A goroutine is a lightweight thread of execution managed by the Go runtime. They're incredibly cheap to create: you can run thousands or even millions of goroutines without significant overhead. ### Creating a Goroutine [#creating-a-goroutine] Creating a goroutine is as simple as adding the `go` keyword before a function call: ```go title="basic-goroutine.go"package main import ( "fmt" "time" ) func sayHello(name string) { fmt.Printf("Hello, %s!\n", name) } func main() { // Regular function call (runs synchronously) sayHello("Alice") // Goroutine call (runs concurrently) go sayHello("Bob") // Give the goroutine time to finish time.Sleep(time.Millisecond) } ``` ### Anonymous Functions with Goroutines [#anonymous-functions-with-goroutines] You can also start goroutines with anonymous functions: ```go title="anonymous-goroutine.go"package main import ( "fmt" "time" ) func main() { go func(msg string) { fmt.Println(msg) }("Hello from goroutine!") // Wait for goroutine to finish time.Sleep(time.Millisecond) } ``` ### Multiple Goroutines [#multiple-goroutines] Let's see multiple goroutines running concurrently: ```go title="multiple-goroutines.go"package main import ( "fmt" "time" ) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("Worker %d started job %d\n", id, j) time.Sleep(time.Second) // Simulate work fmt.Printf("Worker %d finished job %d\n", id, j) results <- j * 2 } } func main() { jobs := make(chan int, 100) results := make(chan int, 100) // Start 3 workers for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // Send 5 jobs for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // Collect results for a := 1; a <= 5; a++ { <-results } } ``` ## What Are Channels? [#what-are-channels] Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values in another goroutine. ### Creating and Using Channels [#creating-and-using-channels] ```go title="basic-channel.go"package main import "fmt" func main() { // Create a channel messages := make(chan string) // Send a value in a goroutine go func() { messages <- "ping" }() // Receive the value msg := <-messages fmt.Println(msg) } ``` ### Channel Directions [#channel-directions] You can specify channel direction to make your code clearer: ```go title="channel-directions.go"package main import "fmt" // Only sends to channel func sender(ch chan<- string) { ch <- "Hello" close(ch) } // Only receives from channel func receiver(ch <-chan string) { for msg := range ch { fmt.Println(msg) } } func main() { ch := make(chan string) go sender(ch) receiver(ch) } ``` ### Buffered Channels [#buffered-channels] By default, channels are unbuffered, meaning they only accept sends when there's a corresponding receive. Buffered channels accept a limited number of values without a receiver: ```go title="buffered-channels.go"package main import "fmt" func main() { // Create a buffered channel with capacity 2 ch := make(chan string, 2) // Sends won't block because channel is buffered ch <- "first" ch <- "second" // Receive values fmt.Println(<-ch) // "first" fmt.Println(<-ch) // "second" } ``` ## Synchronization with Channels [#synchronization-with-channels] Channels are excellent for synchronizing goroutines: ```go title="synchronization.go"package main import ( "fmt" "time" ) func worker(done chan bool) { fmt.Print("Working...") time.Sleep(time.Second) fmt.Println("done") // Signal that work is done done <- true } func main() { done := make(chan bool) go worker(done) // Wait for worker to finish <-done } ``` ## Select Statement [#select-statement] The `select` statement lets a goroutine wait on multiple communication operations: ```go title="select-statement.go"package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(time.Second) c1 <- "one" }() go func() { time.Sleep(2 * time.Second) c2 <- "two" }() // Wait for both values, handling whichever arrives first for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("Received:", msg1) case msg2 := <-c2: fmt.Println("Received:", msg2) } } } ``` ### Select with Timeout [#select-with-timeout] ```go title="select-timeout.go"package main import ( "fmt" "time" ) func main() { c1 := make(chan string) go func() { time.Sleep(2 * time.Second) c1 <- "result" }() select { case res := <-c1: fmt.Println(res) case <-time.After(time.Second): fmt.Println("Timeout!") } } ``` ## Real-World Examples [#real-world-examples] ### Concurrent Web Scraping [#concurrent-web-scraping] ```go title="web-scraper.go"package main import ( "fmt" "net/http" "sync" "time" ) func fetchURL(url string, wg *sync.WaitGroup) { defer wg.Done() start := time.Now() resp, err := http.Get(url) if err != nil { fmt.Printf("Error fetching %s: %v\n", url, err) return } defer resp.Body.Close() fmt.Printf("Fetched %s (status: %d) in %v\n", url, resp.StatusCode, time.Since(start)) } func main() { urls := []string{ "https://go.dev", "https://github.com", "https://golang.org", } var wg sync.WaitGroup for _, url := range urls { wg.Add(1) go fetchURL(url, &wg) } wg.Wait() fmt.Println("All URLs fetched!") } ``` ### Worker Pool Pattern [#worker-pool-pattern] ```go title="worker-pool.go"package main import ( "fmt" "sync" "time" ) type Job struct { ID int Payload string Processed bool } type Result struct { JobID int Output string Error error } func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) { defer wg.Done() for job := range jobs { fmt.Printf("Worker %d processing job %d\n", id, job.ID) // Simulate processing time.Sleep(time.Millisecond * 100) results <- Result{ JobID: job.ID, Output: fmt.Sprintf("Processed job %d", job.ID), Error: nil, } } } func main() { numJobs := 10 numWorkers := 3 jobs := make(chan Job, numJobs) results := make(chan Result, numJobs) var wg sync.WaitGroup // Start workers for w := 1; w <= numWorkers; w++ { wg.Add(1) go worker(w, jobs, results, &wg) } // Send jobs for j := 1; j <= numJobs; j++ { jobs <- Job{ ID: j, Payload: fmt.Sprintf("Job payload %d", j), } } close(jobs) // Wait for workers to finish go func() { wg.Wait() close(results) }() // Collect results for result := range results { if result.Error != nil { fmt.Printf("Job %d failed: %v\n", result.JobID, result.Error) } else { fmt.Printf("Job %d succeeded: %s\n", result.JobID, result.Output) } } } ``` ### Pipeline Pattern [#pipeline-pattern] ```go title="pipeline.go"package main import "fmt" // First stage: generate numbers func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // Second stage: square numbers func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // Third stage: multiply by 2 func multiplyByTwo(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * 2 } close(out) }() return out } func main() { // Set up the pipeline numbers := generate(1, 2, 3, 4, 5) squared := square(numbers) doubled := multiplyByTwo(squared) // Consume the output for result := range doubled { fmt.Println(result) // 2, 8, 18, 32, 50 } } ``` ### Fan-Out/Fan-In Pattern [#fan-outfan-in-pattern] ```go title="fanout-fanin.go"package main import ( "fmt" "sync" "time" ) func producer(id int, out chan<- int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 3; i++ { out <- id*10 + i } } func consumer(in <-chan int, done chan<- bool) { for value := range in { fmt.Printf("Consumed: %d\n", value) time.Sleep(time.Millisecond * 50) } done <- true } func main() { // Fan-out: distribute work to multiple goroutines source := make(chan int) var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go producer(i, source, &wg) } // Close source channel when producers finish go func() { wg.Wait() close(source) }() // Fan-in: collect results from multiple goroutines done := make(chan bool) go consumer(source, done) <-done fmt.Println("All done!") } ``` ## Context for Cancellation [#context-for-cancellation] Go's `context` package is essential for managing goroutine lifecycles: ```go title="context-cancellation.go"package main import ( "context" "fmt" "time" ) func worker(ctx context.Context, id int) { for { select { case <-ctx.Done(): fmt.Printf("Worker %d: shutting down\n", id) return default: fmt.Printf("Worker %d: working\n", id) time.Sleep(500 * time.Millisecond) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) // Start workers for i := 1; i <= 3; i++ { go worker(ctx, i) } // Let them work for a bit time.Sleep(time.Second * 2) // Cancel all goroutines fmt.Println("Cancellation signal sent!") cancel() // Wait for clean shutdown time.Sleep(time.Second) } ``` ### Context with Timeout [#context-with-timeout] ```go title="context-timeout.go"package main import ( "context" "fmt" "time" ) func task(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("Task cancelled:", ctx.Err()) return default: fmt.Println("Task working...") time.Sleep(500 * time.Millisecond) } } } func main() { // Create context with 2 second timeout ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go task(ctx) // Wait for timeout time.Sleep(3 * time.Second) } ``` ## Best Practices [#best-practices] ### 1. Always Close Channels [#1-always-close-channels] The sender should close the channel when done: ```go title="close-channel.go"package main import "fmt" func producer(ch chan<- int) { for i := 0; i < 5; i++ { ch <- i } close(ch) // Always close when done } func main() { ch := make(chan int) go producer(ch) // Range automatically stops when channel is closed for value := range ch { fmt.Println(value) } } ``` ### 2. Never Close from Receiver [#2-never-close-from-receiver] Only the sender should close the channel, never the receiver: ```go title="wrong-close.go"// DON'T DO THIS func consumer(ch <-chan int) { // Error: cannot close receive-only channel close(ch) } ``` ### 3. Use WaitGroups for Multiple Goroutines [#3-use-waitgroups-for-multiple-goroutines] ```go title="waitgroup.go"package main import ( "fmt" "sync" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("Worker %d starting\n", id) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Add(1) go worker(i, &wg) } wg.Wait() fmt.Println("All workers finished") } ``` ### 4. Avoid Goroutine Leaks [#4-avoid-goroutine-leaks] Always ensure goroutines can exit: ```go title="goroutine-leak.go"// BAD: Goroutine leak func bad() { ch := make(chan int) go func() { value := <-ch // Will block forever if nothing is sent fmt.Println(value) }() // Function returns, goroutine blocked forever } // GOOD: Goroutine can exit func good() { ch := make(chan int) go func() { value, ok := <-ch if !ok { return // Channel closed, exit } fmt.Println(value) }() close(ch) // Signal goroutine to exit } ``` ## Common Pitfalls [#common-pitfalls] ### 1. Data Races [#1-data-races] ```go title="data-race.go"// BAD: Data race package main import ( "fmt" "sync" ) func main() { var counter int var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // Data race! }() } wg.Wait() fmt.Println(counter) // Unpredictable result } ``` **Fix with mutex:** ```go title="data-race-fix.go"// GOOD: Using mutex package main import ( "fmt" "sync" ) func main() { var counter int var wg sync.WaitGroup var mu sync.Mutex for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() counter++ mu.Unlock() }() } wg.Wait() fmt.Println(counter) // Correct result: 1000 } ``` ### 2. Forgetting to Wait [#2-forgetting-to-wait] ```go title="forgotten-wait.go"// BAD: Main exits before goroutine finishes func bad() { go func() { fmt.Println("Goroutine running") time.Sleep(time.Second) fmt.Println("Goroutine done") }() // Program exits immediately } // GOOD: Wait for goroutine func good() { done := make(chan bool) go func() { fmt.Println("Goroutine running") time.Sleep(time.Second) fmt.Println("Goroutine done") done <- true }() <-done // Wait for signal } ``` ## Conclusion [#conclusion] Goroutines and channels are Go's answer to concurrency, making it easy to write concurrent programs that are both efficient and correct. Remember: * **Goroutines** are lightweight threads for concurrent execution * **Channels** are for communication and synchronization between goroutines * **Use `select`** for waiting on multiple operations * **Always close channels** from the sender side * **Use context** for managing goroutine lifecycles * **Be careful of data races** and goroutine leaks With these tools and patterns, you can build highly concurrent applications that are both performant and maintainable. # A Simple Guide to React Error Boundaries Error Boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the crashed component tree. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them. **NOTE:** Error boundaries only catch errors in the components **below** them in the tree. An error boundary can't catch an error within itself. This post assumes you have a basic understanding of React and JavaScript. **Important Note on Component Types:** Error boundaries require class components because they rely on lifecycle methods (`static getDerivedStateFromError` and `componentDidCatch`) that aren't available in functional components. This is still true in React 19: React does **not** ship a built-in error boundary component. If you want a functional-style API, the community-standard [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary) library wraps a class boundary for you. This guide covers writing your own class boundary and using the library. ## What Errors Do Error Boundaries Catch? [#what-errors-do-error-boundaries-catch] Error boundaries catch JavaScript errors that occur: 1. **During rendering** 2. **In lifecycle methods** 3. **In constructors** 4. **In event handlers** (with a different approach) ## What Errors Don't They Catch? [#what-errors-dont-they-catch] Error boundaries **do not** catch errors for: 1. **Event handlers** - Use regular try/catch instead 2. **Asynchronous code** (e.g., setTimeout, promise callbacks) 3. **Server-side rendering** 4. **Errors thrown in the error boundary itself** ## Creating a Basic Error Boundary [#creating-a-basic-error-boundary] ### Functional API with `react-error-boundary` [#functional-api-with-react-error-boundary] React has no built-in error boundary component, not in React 18 and not in React 19. The most popular way to get a functional-friendly API is the [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary) library, which provides a ready-made `` component you can drop in: ```tsx title="react-error-boundary-lib.tsx" import { ErrorBoundary } from 'react-error-boundary'; function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) { return (

Something went wrong!

{error.message}

); } function App() { return ( ); } ``` Under the hood `react-error-boundary` is still a class component. It just hides the boilerplate behind a clean, functional-first API (`FallbackComponent`, `onError`, `onReset`, and a `useErrorBoundary` hook for throwing async errors). ### React 19 Root-Level Error Handlers [#react-19-root-level-error-handlers] React 19 didn't add an error boundary component, but it did add root-level error callbacks so you can log every error in one place. Pass `onCaughtError` and `onUncaughtError` when you create the root: ```tsx title="react-19-root-handlers.tsx" import { createRoot } from 'react-dom/client'; import App from './App'; const root = createRoot(document.getElementById('root')!, { // Fired for errors that an error boundary caught onCaughtError: (error, errorInfo) => { console.error('Caught by a boundary:', error, errorInfo.componentStack); }, // Fired for errors no boundary caught onUncaughtError: (error, errorInfo) => { console.error('Not caught by any boundary:', error, errorInfo.componentStack); } }); root.render(); ``` These callbacks complement error boundaries. They don't replace them. You still need a boundary (your own class or `react-error-boundary`) to render a fallback UI. ### Writing Your Own Class Component Error Boundary [#writing-your-own-class-component-error-boundary] **Note:** If you'd rather not add a dependency, you write the boundary yourself. It must be a class component. This is a React requirement (still true in React 19) because error boundaries need access to lifecycle methods that aren't available in functional components. Class component error boundaries implement either or both of these lifecycle methods: * `static getDerivedStateFromError()` - For rendering a fallback UI * `componentDidCatch()` - For logging error information Here's a basic error boundary for React 18 and earlier: ```tsx title="basic-error-boundary.tsx" twoslash import * as React from 'react'; type ErrorBoundaryState = { hasError: boolean; error?: Error; } type ErrorBoundaryProps = { children: React.ReactNode; fallback?: React.ReactNode; } /** * Error Boundary for React 18 and earlier * NOTE: Class components are required for error boundaries in React < 19 * because they need access to static getDerivedStateFromError and componentDidCatch */ class ErrorBoundary extends React.Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { // Update state so the next render will show the fallback UI return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { // You can also log the error to an error reporting service console.error('Error caught by boundary:', error, errorInfo); // Example: Log to error reporting service // logErrorToService(error, errorInfo); } render() { if (this.state.hasError) { // You can render any custom fallback UI return ( this.props.fallback || (

Something went wrong.

{this.state.error?.message}

) ); } return this.props.children; } } export default ErrorBoundary; ``` ## Using Error Boundaries [#using-error-boundaries] You can wrap any component with an error boundary: ```tsx title="using-error-boundary.tsx" import * as React from 'react'; import ErrorBoundary from './ErrorBoundary'; function BuggyComponent() { // This will throw an error throw new Error('I crashed!'); return
This will never render
; } function App() { return ( ); } ``` ## Custom Fallback UI [#custom-fallback-ui] You can provide a custom fallback component: ```tsx title="custom-fallback.tsx" twoslash import * as React from 'react'; type ErrorFallbackProps = { error: Error; resetError?: () => void; } function ErrorFallback({ error, resetError }: ErrorFallbackProps) { return (

Oops! Something went wrong

{error.message}

{resetError && ( )}
); } // Enhanced Error Boundary with reset functionality /** * NOTE: Error boundaries must be class components in every React version. * Consider the react-error-boundary library if you prefer a functional API. */ type EnhancedErrorBoundaryProps = { children: React.ReactNode; fallback?: (error: Error, resetError: () => void) => React.ReactNode; }; type EnhancedErrorBoundaryState = { hasError: boolean; error?: Error; } class EnhancedErrorBoundary extends React.Component { constructor(props: EnhancedErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): EnhancedErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { console.error('Error caught:', error, errorInfo); } resetError = () => { this.setState({ hasError: false, error: undefined }); }; render() { if (this.state.hasError && this.state.error) { if (this.props.fallback) { return this.props.fallback(this.state.error, this.resetError); } return ( ); } return this.props.children; } } ``` ## Functional Error Boundary with Hooks [#functional-error-boundary-with-hooks] **Important:** You cannot create a true error boundary using only hooks and functional components, in any React version. Error boundaries require access to `static getDerivedStateFromError` and `componentDidCatch` lifecycle methods, which are only available in class components. However, you can create a simplified error handling component using hooks for specific use cases like handling manually thrown errors: ```tsx title="functional-error-handler.tsx" twoslash import * as React from 'react'; interface ErrorHandlerProps { children: React.ReactNode; fallback: (error: Error, reset: () => void) => React.ReactNode; } interface ErrorHandlerState { error: Error | null; } /** * NOTE: This is NOT a true error boundary and won't catch rendering errors. * Use a class component or the react-error-boundary library for actual error boundaries. * This component only handles manually thrown errors via the throwError prop. */ export function ErrorHandler({ children, fallback }: ErrorHandlerProps) { const [error, setError] = React.useState(null); // This is a simplified version for demonstration // It cannot catch rendering errors like a true error boundary if (error) { return fallback(error, () => setError(null)); } return <>{children}; } ``` **Recommendation:** Write a class component when you want full control, or reach for the `react-error-boundary` library when you want a functional API with reset support built in. This applies to React 19 as well: there is no built-in error boundary component to fall back on. ## Handling Errors in Event Handlers [#handling-errors-in-event-handlers] Error boundaries don't catch errors in event handlers. Use try/catch for those: ```tsx title="event-handler-errors.tsx" twoslash import * as React from 'react'; function ButtonComponent() { const handleClick = () => { try { // Code that might throw an error if (Math.random() > 0.5) { throw new Error('Random error occurred!'); } console.log('Success!'); } catch (error) { console.error('Caught in event handler:', error); // You can update state to show an error message } }; return ; } ``` ## Handling Asynchronous Errors [#handling-asynchronous-errors] For async errors, wrap your async operations: ```tsx title="async-errors.tsx" twoslash import * as React from 'react'; function AsyncComponent() { const [error, setError] = React.useState(null); const [data, setData] = React.useState(null); const fetchData = async () => { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error('Failed to fetch data'); } const result = await response.json(); setData(result); } catch (err) { setError(err as Error); } }; if (error) { return
Error: {error.message}
; } return (
{data &&
{data}
}
); } ``` ## Where to Place Error Boundaries [#where-to-place-error-boundaries] You can place error boundaries at different levels of your component tree: ```tsx title="error-boundary-placement.tsx" import * as React from 'react'; import ErrorBoundary from './ErrorBoundary'; function Sidebar() { return
Sidebar Content
; } function MainContent() { // This component might crash throw new Error('Main content crashed!'); return
Main Content
; } function Header() { return
Header
; } function App() { return (
{/* Header has its own error boundary */} Header Error
}>
{/* Sidebar is protected separately */} Sidebar Error
}> {/* Main content is protected separately */} Main Content Error
}> ); } ``` ## Real-World Examples [#real-world-examples] ### Protecting a Data Fetching Component [#protecting-a-data-fetching-component] ```tsx title="data-fetching-protection.tsx" import * as React from 'react'; import ErrorBoundary from './ErrorBoundary'; interface User { id: number; name: string; email: string; } function UserProfile({ userId }: { userId: number }) { const [user, setUser] = React.useState(null); const [loading, setLoading] = React.useState(true); React.useEffect(() => { fetch(`/api/users/${userId}`) .then((res) => { if (!res.ok) throw new Error('User not found'); return res.json(); }) .then((data) => { setUser(data); setLoading(false); }) .catch((error) => { setLoading(false); throw error; // This will be caught by the error boundary }); }, [userId]); if (loading) return
Loading...
; if (!user) return null; return (

{user.name}

{user.email}

); } function App() { return ( (

Failed to load user profile

{error.message}

)} >
); } ``` ### Protecting a Form Component [#protecting-a-form-component] ```tsx title="form-protection.tsx" import * as React from 'react'; import ErrorBoundary from './ErrorBoundary'; interface FormData { name: string; email: string; } function ComplexForm() { const [formData, setFormData] = React.useState({ name: '', email: '' }); // Simulating a complex form that might have rendering issues const validateEmail = (email: string) => { if (!email.includes('@')) { throw new Error('Invalid email format'); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); try { validateEmail(formData.email); // Submit form data } catch (error) { // Handle error in event handler (won't be caught by boundary) alert((error as Error).message); } }; return (
setFormData({ ...formData, name: e.target.value })} placeholder="Name" /> setFormData({ ...formData, email: e.target.value })} placeholder="Email" />
); } function App() { return (

Form Error

Please refresh the page and try again.

} >
); } ``` ### Protecting Third-Party Components [#protecting-third-party-components] ```tsx title="third-party-protection.tsx" import * as React from 'react'; import UnstableThirdPartyComponent from 'some-library'; import ErrorBoundary from './ErrorBoundary'; function App() { return (

My App

{/* Protect the entire app from third-party component errors */}

Third-party component failed

The component encountered an error. Please try again later.

} > ); } ``` ## Logging Errors to a Service [#logging-errors-to-a-service] You can integrate error boundaries with error tracking services: ```tsx title="error-logging.tsx" import * as React from 'react'; import * as Sentry from '@sentry/react'; interface LoggingErrorBoundaryProps { children: React.ReactNode; fallback?: React.ReactNode; } interface LoggingErrorBoundaryState { hasError: boolean; error?: Error; } /** * Logging Error Boundary * NOTE: Error boundaries must be class components in every React version. * The react-error-boundary library exposes the same idea via an onError callback. */ class LoggingErrorBoundary extends React.Component { constructor(props: LoggingErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): LoggingErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { // Log to Sentry Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack } } }); // Or log to your own service // logErrorToService({ // error: error.toString(), // errorInfo: errorInfo.componentStack, // timestamp: new Date().toISOString() // }); } render() { if (this.state.hasError) { return ( this.props.fallback || (

Something went wrong.

The error has been logged. We'll look into it!

) ); } return this.props.children; } } ``` ## Best Practices [#best-practices] 1. **Place error boundaries strategically** - Wrap components that are likely to fail 2. **Don't overuse** - Not every component needs its own error boundary 3. **Provide helpful fallback UI** - Give users a way to recover or navigate away 4. **Log errors** - Always log errors to a service for debugging 5. **Test error boundaries** - Make sure they work as expected 6. **Reset state when possible** - Allow users to retry after an error ## Conclusion [#conclusion] Error boundaries are an essential tool for building robust React applications. They provide a graceful way to handle errors in your component tree and prevent the entire app from crashing. By strategically placing error boundaries and providing helpful fallback UIs, you can improve user experience and make debugging easier. ### Choosing the Right Approach [#choosing-the-right-approach] **Roll your own class component when:** * You want zero dependencies and full control over the boundary's behaviour * The examples in this guide provide production-ready implementations to start from **Reach for `react-error-boundary` when:** * You prefer a functional-first API (`FallbackComponent`, `onError`, `onReset`) * You want built-in reset support and a `useErrorBoundary` hook for async errors Either way, remember there is **no** built-in error boundary component in React, including React 19. What React 19 *does* add is root-level `onCaughtError` / `onUncaughtError` callbacks for centralized logging. Remember: * Error boundaries catch errors in rendering, lifecycle methods, and constructors * They don't catch errors in event handlers or async code (use try/catch for those) * You can use them at any level in your component tree * Always log errors to help with debugging * The boundary itself must be a class component (or a library that wraps one) in every React version # A Simple Guide to TypeScript Generics Generics are a powerful feature in TypeScript that allows you to write reusable, flexible, and type-safe code by creating components and functions that work with a variety of types while maintaining type safety. Instead of writing multiple functions that do the same thing but with different types, you can write a single generic function that adapts to the type it receives. **NOTE:** This post assumes you have a basic understanding of TypeScript and JavaScript. Generics can be complex at first, but once you understand the concept, they become an invaluable tool in your TypeScript toolkit. ## The Problem Without Generics [#the-problem-without-generics] Before diving into generics, let's look at why we need them. Consider a function that returns whatever is passed to it: ```ts title="without-generics.ts" twoslash function identity(arg: string): string { return arg; } function identityNumber(arg: number): number { return arg; } function identityBoolean(arg: boolean): boolean { return arg; } ``` This is repetitive and not scalable. We could use `any`, but we lose type safety: ```ts title="using-any.ts" twoslash function identity(arg: any): any { return arg; } const output = identity('hello'); // We don't know what type output is, which defeats TypeScript's purpose ``` ## Basic Generic Syntax [#basic-generic-syntax] With generics, we can create a single function that works with any type while preserving type information: ```ts title="basic-generic.ts" twoslash function identity(arg: T): T { return arg; } // TypeScript can infer the type const stringResult = identity('hello'); // type is string const numberResult = identity(42); // type is number const booleanResult = identity(true); // type is boolean // You can also explicitly specify the type const explicitResult = identity('hello'); // type is string ``` The `` is a type variable - a placeholder for a type that will be specified when the function is called. You can think of it like a function argument, but for types instead of values. ## Generic Interfaces [#generic-interfaces] You can also use generics with interfaces: ```ts title="generic-interface.ts" twoslash interface Box { contents: T; } const stringBox: Box = { contents: 'Hello, Generics!' }; const numberBox: Box = { contents: 42 }; // You can even nest generics const boxBox: Box> = { contents: { contents: 'Nested box!' } }; ``` ## Generic Classes and Functional Alternatives [#generic-classes-and-functional-alternatives] While classes can be generic, modern TypeScript/JavaScript often prefers functional patterns: ### Class-based approach (traditional): [#class-based-approach-traditional] ```ts title="generic-class.ts" twoslash class Storage { private items: T[] = []; add(item: T): void { this.items.push(item); } get(index: number): T | undefined { return this.items[index]; } getAll(): T[] { return [...this.items]; } } // Usage const stringStorage = new Storage(); stringStorage.add('Item 1'); stringStorage.add('Item 2'); const numberStorage = new Storage(); numberStorage.add(1); numberStorage.add(2); ``` ### Functional approach (modern): [#functional-approach-modern] ```ts title="generic-functional.ts" twoslash // Factory function with generics function createStorage() { let items: T[] = []; return { add(item: T): void { items.push(item); }, get(index: number): T | undefined { return items[index]; }, getAll(): T[] { return [...items]; } }; } // Usage - same as class but with functional pattern const stringStorage = createStorage(); stringStorage.add('Item 1'); stringStorage.add('Item 2'); const numberStorage = createStorage(); numberStorage.add(1); numberStorage.add(2); ``` ## Generic Constraints [#generic-constraints] Sometimes you want to constrain a generic to only work with certain types. You can do this using the `extends` keyword: ```ts title="generic-constraint.ts" twoslash interface Lengthwise { length: number; } function logLength(arg: T): void { console.log(arg.length); } logLength('hello'); // Works - strings have a length property logLength([1, 2, 3]); // Works - arrays have a length property logLength({ length: 10, value: 'test' }); // Works - has length property // logLength(42); // Error - numbers don't have a length property ``` You can also constrain to specific object shapes: ```ts title="constraint-properties.ts" twoslash interface WithId { id: number; } function findById(items: T[], id: number): T | undefined { return items.find((item) => item.id === id); } const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]; const user = findById(users, 1); // type is { id: number; name: string; } | undefined ``` ## Using Type Parameters in Generic Constraints [#using-type-parameters-in-generic-constraints] You can reference one type parameter in the constraint of another: ```ts title="type-parameter-constraint.ts" twoslash function getProperty(obj: T, key: K): T[K] { return obj[key]; } const user = { name: 'Alice', age: 30, email: 'alice@example.com' }; const name = getProperty(user, 'name'); // type is string const age = getProperty(user, 'age'); // type is number // const invalid = getProperty(user, "invalid"); // Error - "invalid" is not a key of user ``` This is extremely powerful for creating type-safe property access. ## Generic Utility Types [#generic-utility-types] TypeScript provides several built-in generic utility types: ### `Partial` - Makes all properties optional [#partialt---makes-all-properties-optional] ```ts title="partial.ts" twoslash interface User { id: number; name: string; email: string; } function updateUser(id: number, updates: Partial): void { // updates can have any subset of User properties const user: User = { id, name: '', email: '' }; Object.assign(user, updates); } updateUser(1, { name: 'New Name' }); // Works ``` ### `Required` - Makes all properties required [#requiredt---makes-all-properties-required] ```ts title="required.ts" twoslash interface PartialUser { id?: number; name?: string; email?: string; } type FullUser = Required; // All properties are now required ``` ### `Pick` - Select specific properties [#pickt-k---select-specific-properties] ```ts title="pick.ts" twoslash interface User { id: number; name: string; email: string; password: string; } type PublicUser = Pick; // PublicUser has only id, name, and email ``` ### `Omit` - Remove specific properties [#omitt-k---remove-specific-properties] ```ts title="omit.ts" twoslash interface User { id: number; name: string; email: string; password: string; } type PublicUser = Omit; // PublicUser has everything except password ``` ## Real-World Example: API Response Handler [#real-world-example-api-response-handler] Here's a practical example of using generics for API handling: ```ts title="api-response.ts" twoslash interface ApiResponse { data: T; status: number; message: string; } interface User { id: number; name: string; email: string; } interface Post { id: number; title: string; content: string; } async function fetchApi(url: string): Promise> { const response = await fetch(url); const data = await response.json(); return { data, status: response.status, message: response.statusText }; } // Usage async function getUser(id: number) { const response = await fetchApi(`/api/users/${id}`); // response.data is typed as User console.log(response.data.name); } async function getPost(id: number) { const response = await fetchApi(`/api/posts/${id}`); // response.data is typed as Post console.log(response.data.title); } ``` ## Real-World Example: Generic Repository Pattern [#real-world-example-generic-repository-pattern] Using a functional approach with TypeScript generics: ```ts title="repository.ts" twoslash interface Entity { id: number; } // Functional repository definition using TypeScript generics interface Repository { findById(id: number): Promise; findAll(): Promise; create(entity: Omit): Promise; update(id: number, updates: Partial): Promise; delete(id: number): Promise; } // Factory function to create a repository function createRepository(initialData: T[] = []): Repository { let items: T[] = [...initialData]; return { async findById(id: number): Promise { return items.find((item) => item.id === id) || null; }, async findAll(): Promise { return [...items]; }, async create(entity: Omit): Promise { const newEntity: T = { id: items.length + 1, ...entity } as T; items.push(newEntity); return newEntity; }, async update(id: number, updates: Partial): Promise { const index = items.findIndex((item) => item.id === id); if (index === -1) return null; items[index] = { ...items[index], ...updates }; return items[index]; }, async delete(id: number): Promise { const index = items.findIndex((item) => item.id === id); if (index === -1) return false; items.splice(index, 1); return true; } }; } // Usage example interface User extends Entity { name: string; email: string; } // Create a user repository using the factory function const userRepository = createRepository([{ id: 1, name: 'Alice', email: 'alice@example.com' }]); // Use the repository methods async function example() { const user = await userRepository.findById(1); const allUsers = await userRepository.findAll(); const newUser = await userRepository.create({ name: 'Bob', email: 'bob@example.com' }); const updated = await userRepository.update(1, { name: 'Alice Updated' }); const deleted = await userRepository.delete(1); } ``` ## Best Practices [#best-practices] 1. **Prefer functional patterns**: Modern TypeScript/JavaScript favors functional programming patterns over classes. Use factory functions and composition over class inheritance where possible. 2. **Use descriptive type parameter names**: While `T` is conventional, use more descriptive names when appropriate: ```ts // For multiple type parameters // For key-value pairs // For React components ``` 3. **Use constraints when needed**: Don't make your generic too permissive. Use `extends` to enforce requirements. 4. **Consider defaults**: You can provide default types for generics: ```ts interface Box { contents: T; } const defaultBox: Box = { contents: 'hello' }; // T is string ``` 5. **Don't overuse**: Not everything needs to be generic. Use generics when you genuinely need type flexibility. ## Conclusion [#conclusion] Generics are a fundamental tool in TypeScript that allows you to write reusable, type-safe code. They enable you to create functions, interfaces, and classes that work with a variety of types while maintaining full type safety. By mastering generics, you'll write more maintainable and flexible code that catches errors at compile time rather than runtime. # HTTP/HTTP2/HTTP3 The **HyperText Transfer Protocol (HTTP)** is the foundation of data communication on the web. It defines how messages are formatted and transmitted, and how web servers and browsers should respond to various commands. *** ## HTTP/1.1 [#http11] **Released**: 1997 **Key Features**: * Text-based protocol * One TCP connection per request * Head-of-line blocking * Supports caching, persistent connections (keep-alive), and chunked transfer ### Example Request [#example-request] ```http GET /index.html HTTP/1.1 Host: example.com ``` ## HTTP/2 [#http2] **Released**: 2015 Built On: TCP Key Improvements: * Binary framing layer * Multiplexing (multiple requests in a single TCP connection) * Header compression (HPACK) * Server push (proactively sending resources) ### Benefits Over HTTP/1.1 [#benefits-over-http11] * Reduced latency. * Better use of a single TCP connection. * Fewer round trips. ## HTTP/3 [#http3] **Released**: 2022 Built On: QUIC (UDP-based) Key Features: * Fully multiplexed using QUIC streams. * Connection migration (better for mobile). * TLS 1.3 integrated by default. * Eliminates head-of-line blocking at the transport layer. ### Why It Matters [#why-it-matters] HTTP/3 addresses long-standing issues from TCP-based protocols. It's designed to be faster and more reliable, especially on unreliable or mobile networks. | Feature | HTTP/1.1 | HTTP/2 | HTTP/3 | | -------------------- | -------- | ------ | ----------- | | Protocol Layer | TCP | TCP | QUIC (UDP) | | Multiplexing | ❌ | ✔️ | ✔️ | | Header Compression | ❌ | HPACK | QPACK | | Server Push | ❌ | ✔️ | ✔️(limited) | | Connection Migration | ❌ | ❌ | ✔️(limited) | HTTP/3 is already supported by major platforms like Google, Facebook, and Cloudflare. It's becoming the new default for high-performance web communication. # You Might Not Need Redis or Kafka Yet, Postgres as a Cache and a Message Queue ## Two problems every backend meets [#two-problems-every-backend-meets] Most backends start the same way: an application and a database. Then, sooner or later, two things happen. **The first is a slow endpoint.** A page that lists courses, products or reports runs a query with several joins and a count. It takes a few hundred milliseconds, it is requested constantly, and its answer barely changes from one minute to the next. Running the same expensive query again and again is plainly wasteful. **The second is work that should not happen inside a request.** A user registers and you send a confirmation email. The email provider is slow today, so the registration request hangs. Or the provider is down for ten seconds, the send fails, and the user is registered but never told. Both problems have well-known answers. For the first you add a cache, and the default choice is Redis. For the second you add a queue, and the default choice is a message broker such as RabbitMQ or Kafka. Those answers are correct. They also turn one moving part into three. Each new service has to be deployed, secured, monitored, upgraded, backed up, and run on every developer's laptop. For a large system that cost is small next to the benefit. For a small or medium one, it is often the biggest thing on the bill. On a platform I worked on recently, the application shipped as a single binary with Postgres as its only hard dependency, and I wanted to keep it that way. So I solved both problems with the database that was already there. This post covers: * How the cache works, and how it compares with Redis and Memcached. * How the queue works, and how it compares with Kafka, RabbitMQ, SQS, Redis and NATS. * What is happening inside Postgres that makes each technique work, and what it costs. * How to decide, and the signs that you have outgrown it. **A note on language.** The examples are written in Go because that is what the project used. The solution is not a Go solution. The part that does the work is SQL, and it behaves the same from Node.js, Python, Java, Ruby, C# or anything else with a Postgres driver. Near the end there is a section that maps each Go detail to its equivalent elsewhere. If you do want background on the Go pieces, I have written about [context](/blog/guide-to-context-in-golang) and [goroutines and channels](/blog/guide-to-goroutines-and-channels). ## First, what the traditional tools are for [#first-what-the-traditional-tools-are-for] It helps to name what each dedicated tool is built to do before deciding whether Postgres can stand in for it. ### Key-value stores [#key-value-stores] A key-value store answers one question very fast: "what is stored under this key?" The two you will meet most are Redis and Memcached. | Tool | What it is | Known for | | --------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | Redis | An in-memory data structure server with optional persistence to disk | Strings, hashes, lists, sets, sorted sets, streams, pub/sub, TTL on every key | | Memcached | An in-memory cache and nothing else | Simple get and set, multi-threaded, least-recently-used eviction, no disk | Both keep data in memory, which is why a read is typically answered in well under a millisecond on a local network. Both can be told how much memory they may use, and both evict old entries on their own when that limit is reached. ### Messaging systems [#messaging-systems] Messaging tools fall into two families that are easy to confuse. A **queue** hands each message to one worker. When the worker confirms it is done, the message is gone. This is for distributing work: send this email, resize this image, charge this card. A **log** appends every message to an ordered record that is kept for a set time. Many independent readers each track their own position in it and can go back and read again. This is for streaming events: every order placed, every click, every change to a row. | Tool | Family | What it is | | -------------------- | ----------------- | -------------------------------------------------------------------------------------- | | RabbitMQ | Queue | A broker. Producers publish to exchanges, which route messages into queues by rule | | Amazon SQS | Queue | A fully managed queue. Nothing to run, pay per request | | Apache Kafka | Log | A distributed, partitioned, replicated commit log built for very high throughput | | NATS | Pub/sub and log | A lightweight messaging server. Its JetStream layer adds persistence and replay | | Redis Streams | Log | An append-only stream type inside Redis, with consumer groups | | Redis lists, pub/sub | Queue and pub/sub | The simplest options. Lists work as a basic queue. Pub/sub drops messages nobody hears | Keep those two families in mind. A Postgres table is a good queue. It is a poor log. ## Part one: the cache [#part-one-the-cache] ### The problem: an expensive answer, asked for constantly [#the-problem-an-expensive-answer-asked-for-constantly] An endpoint is slow because its query is expensive, and it is called far more often than its answer changes. The fix is to compute the answer once, keep it somewhere cheap to read, and serve that copy until it is too old or the underlying data changes. "Somewhere cheap to read" is usually taken to mean memory, in another process. It does not have to. It only has to be much cheaper than the query it replaces. Looking up one row by its primary key is among the cheapest things a database does. ### The solution: an UNLOGGED table [#the-solution-an-unlogged-table] ```sql title="migrations/cache.sql" CREATE UNLOGGED TABLE IF NOT EXISTS cache ( key text PRIMARY KEY, value jsonb NOT NULL, expires_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache (expires_at) WHERE expires_at IS NOT NULL; ``` The word doing the work is `UNLOGGED`, and to see why, you need to know what Postgres normally does on a write. Before Postgres changes a table, it appends a description of the change to the **write-ahead log**, a sequential file on disk, and makes sure that record is safely stored before telling you the transaction committed. If the server loses power a moment later, it replays the log on restart and nothing committed is lost. Replicas work by receiving and replaying the same log. This is the foundation of Postgres durability, and it is also a real cost: every write is written twice, once to the log and later to the table itself. An unlogged table opts out. Its changes are not written to the log, so writes are much cheaper. The price has two parts: * The table is emptied after a crash or an unclean shutdown. * Its contents are not sent to standby servers, so you cannot read the cache from a replica. For a cache that is the right trade. Cached data is disposable by definition. Losing it costs one slow request per key. ### Get and set [#get-and-set] ```go title="cache/cache.go" type Cache struct { db *pgxpool.Pool log *slog.Logger group singleflight.Group } // Get returns the value for key, or ok=false on a miss or an expired entry. // An error is treated as a miss, so a cache failure never breaks a request. func (c *Cache) Get(ctx context.Context, key string) (json.RawMessage, bool) { var value json.RawMessage err := c.db.QueryRow(ctx, ` SELECT value FROM cache WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, key, ).Scan(&value) if err != nil { return nil, false } return value, true } // Set stores value under key. A ttl of zero or less means no expiry. func (c *Cache) Set(ctx context.Context, key string, value json.RawMessage, ttl time.Duration) { var expiresAt *time.Time if ttl > 0 { t := time.Now().Add(ttl) expiresAt = &t } ctx, cancel := detach(ctx) defer cancel() _, err := c.db.Exec(ctx, ` INSERT INTO cache (key, value, expires_at) VALUES ($1, $2, $3) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`, key, value, expiresAt, ) if err != nil { c.log.Warn("cache set failed", "key", key, "err", err) } } ``` Two decisions are worth pointing at. Expiry is checked on read. An expired row is simply not returned, so correctness never depends on a cleanup job having run. A cache error is a miss, not a failure. If the cache is unhealthy the request falls through to the real query. A cache that can take the API down is worse than no cache. ### Sweeping expired rows [#sweeping-expired-rows] Redis and Memcached remove expired keys for you. Postgres does not know these rows are a cache, so a small loop does it. ```go title="cache/cache.go" // SweepLoop deletes expired entries until ctx is cancelled. func (c *Cache) SweepLoop(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: if _, err := c.db.Exec(ctx, `DELETE FROM cache WHERE expires_at < now()`); err != nil { c.log.Warn("cache sweep failed", "err", err) } } } } ``` The partial index on `expires_at` is there for this query. The sweep is housekeeping only. If it stops, reads stay correct and the table just grows. ### Detaching from the request [#detaching-from-the-request] `detach` is three lines, and it fixes a bug that is very hard to see. ```go title="cache/cache.go" // detach returns a context that survives the request being cancelled, but // still cannot hang. func detach(ctx context.Context) (context.Context, context.CancelFunc) { return context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) } ``` Think about invalidation. A handler updates a row, then deletes the cache key. If the client disconnects between those two steps, the request context is cancelled and the delete never runs. The database has the new value, the cache has the old one, and nothing will correct it until the TTL runs out. `context.WithoutCancel` keeps the values of the request context but drops its cancellation. The timeout on top means this bookkeeping still cannot hold a shutdown open. ### Stopping the stampede [#stopping-the-stampede] The interesting part of a cache is the moment an entry expires. If fifty requests are in flight for the same key, all fifty miss, and all fifty run the expensive query at once. The cache turns a busy moment into a pile-up. ```go title="cache/cache.go" func (c *Cache) GetOrSet( ctx context.Context, key string, ttl time.Duration, load func() (json.RawMessage, error), ) (json.RawMessage, error) { if v, ok := c.Get(ctx, key); ok { return v, nil } v, err, _ := c.group.Do(key, func() (any, error) { // Check again: the request that filled the key may have finished // while this one waited for its turn. if v, ok := c.Get(ctx, key); ok { return v, nil } v, err := load() if err != nil { return nil, err } c.Set(ctx, key, v, ttl) return v, nil }) if err != nil { return nil, err } return v.(json.RawMessage), nil } ``` [`singleflight`](https://pkg.go.dev/golang.org/x/sync/singleflight) makes concurrent callers for the same key share one call to `load`. The other forty-nine wait and receive the same result. It only covers one process, so three instances can still run the query three times. That is fine. Three is not fifty. This problem is not specific to Postgres. A Redis cache stampedes in exactly the same way, and needs the same fix. ### Using it in a handler [#using-it-in-a-handler] This is the cache-aside pattern. The handler asks the cache, and only does the real work on a miss. ```go title="courses/handler.go" func (h *Handler) ListCourses(w http.ResponseWriter, r *http.Request) { filters := parseFilters(r) // The key is built from the parsed filters, not the raw query string, so // "?a=1&b=2" and "?b=2&a=1" share one entry. key := "courses:list:" + filters.CacheKey() body, err := h.cache.GetOrSet(r.Context(), key, 5*time.Minute, func() (json.RawMessage, error) { courses, err := h.store.ListCourses(r.Context(), filters) if err != nil { return nil, err } return json.Marshal(courses) }) if err != nil { http.Error(w, "could not load courses", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(body) } ``` Storing the finished JSON has a nice side effect. On a hit there is no decoding and no encoding. The bytes go from the row to the response. ### Invalidating a list [#invalidating-a-list] A list endpoint's key has to include its filters, so one write makes a whole family of keys stale: `courses:list:page=1`, `courses:list:page=2&sort=name`, and so on. Nobody can enumerate them. So keys are namespaced with a colon and deleted by prefix. ```go title="cache/cache.go" var likeEscaper = strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`) func (c *Cache) DeletePrefix(ctx context.Context, prefix string) { ctx, cancel := detach(ctx) defer cancel() _, err := c.db.Exec(ctx, `DELETE FROM cache WHERE key LIKE $1`, likeEscaper.Replace(prefix)+"%", ) if err != nil { c.log.Error("cache invalidation failed, entries may be stale", "prefix", prefix, "err", err) } } ``` The escaping matters. In `LIKE`, an underscore matches any single character. Without escaping, clearing `user_roles:` would also clear `userXroles:`. Backslash is replaced first, or the escapes being added would themselves be escaped. This is one place where Postgres is the easier tool. Deleting by pattern in Redis means walking the keyspace with `SCAN`, or maintaining your own set of keys per group. Here it is one indexed `DELETE`. ### Postgres against Redis and Memcached [#postgres-against-redis-and-memcached] | Question | Postgres unlogged table | Redis | Memcached | | ----------------------------- | ------------------------------------------ | --------------------------------------------------- | ---------------------------------- | | Where the data lives | Disk pages, held in Postgres shared memory | Memory | Memory | | Speed of a read | A full SQL round trip | Typically well under a millisecond | Typically well under a millisecond | | Expiry | A column you check and a loop you write | Built in, per key | Built in, per key | | Bounded memory with eviction | No. The table grows until you delete rows | Yes, with a chosen eviction policy | Yes, least recently used | | Survives a restart | A clean restart yes, a crash no | Optional, with snapshots or an append-only file | No | | Readable from replicas | No, unlogged tables are not replicated | Yes | No replication built in | | Delete a group of keys | One `DELETE` with `LIKE` | `SCAN` and delete, or track keys yourself | Not supported | | Same transaction as your data | Yes | No | No | | Richer structures | Anything SQL can express, including JSON | Lists, sets, sorted sets, hashes, streams, and more | Plain values only | | Extra service to run | None | One | One | ### Advantages of caching in Postgres [#advantages-of-caching-in-postgres] * **Nothing new to operate.** No second service to deploy, patch, monitor, secure, or connect to. One connection string, one backup, one set of credentials. * **Local development is trivial.** Anyone who can run the app can run the cache, because it is the app's database. * **Invalidation can join the write.** You can delete a cache row in the same transaction that changes the data, so the two cannot disagree. No other option in the table offers this. * **Pattern deletes are easy.** One statement clears a whole family of keys. * **You can inspect it with SQL.** How many entries, how big, which are oldest, which prefix takes the most space. It is a table. * **The same pool, the same tooling.** Metrics, tracing and query logs you already have cover the cache too. ### Disadvantages of caching in Postgres [#disadvantages-of-caching-in-postgres] * **It is slower per read.** Every hit is a SQL round trip that has to be parsed, planned and executed. For an endpoint that takes 200 milliseconds to compute, that is a huge win. For one that takes two, it may be no win at all. * **It spends database connections.** Cache reads compete with real queries for the same pool. Under load, the cache can be the thing that exhausts it. * **There is no eviction.** Redis and Memcached drop old entries when memory fills. This table grows until your sweep deletes rows. One bug that writes unbounded keys can fill a disk. * **Updates leave dead rows.** Postgres does not overwrite a row in place. Each `UPDATE` writes a new version and leaves the old one for vacuum to clear. A cache with heavy churn creates a lot of that work. * **A crash empties it.** After an unclean shutdown everything is cold at once, and the database absorbs the full load at the moment it has just restarted. * **It shares fate with the database.** A dedicated cache can keep serving when the database is struggling. This one cannot, because it is the database. ## Part two: the queue [#part-two-the-queue] ### The problem: slow work inside a request [#the-problem-slow-work-inside-a-request] Sending during the request ties the caller's latency, and success, to someone else's SMTP server. A slow relay stalls the handler. A brief outage loses the message unless the user tries again. Writing a row first and delivering it in the background separates the two. The API answers as soon as the message is safely recorded, and delivery can be retried without anyone resubmitting anything. Here the queue is the `emails` table itself. Those rows had to be stored anyway as a record of what was sent, so the queue is three extra columns. ```sql title="migrations/emails.sql" CREATE TABLE IF NOT EXISTS emails ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), recipients text[] NOT NULL, subject text NOT NULL, body text NOT NULL DEFAULT '', status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'sending', 'sent', 'failed')), attempts integer NOT NULL DEFAULT 0, claimed_at timestamptz, error text, sent_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); -- The worker's claim query: oldest queued rows first. CREATE INDEX IF NOT EXISTS idx_emails_queue ON emails (created_at) WHERE status = 'queued'; -- The reclaim sweep, which only looks at rows in flight. CREATE INDEX IF NOT EXISTS idx_emails_claimed ON emails (claimed_at) WHERE status = 'sending'; ``` Unlike the cache, this table is a normal logged table. A queued email must survive a crash. Both indexes are partial. The table keeps every email ever sent, but the indexes only hold rows that are queued or in flight, so they stay tiny however much history builds up. ### The biggest advantage: enqueue in the same transaction [#the-biggest-advantage-enqueue-in-the-same-transaction] This is the single strongest reason to queue in your database, and no external broker can match it. Say a registration must be saved and a confirmation email must be sent. With a separate broker you have two systems and two writes. Whichever order you choose, one failure leaves them disagreeing: * Save first, publish second. If the publish fails, the user is registered and never hears about it. * Publish first, save second. If the save fails, the user gets a confirmation for a registration that does not exist. This is known as the dual-write problem. The standard cure is the outbox pattern: write the message into a table in the same transaction as the data, then have something forward it to the broker. When the queue is already a table, you get the outbox for free and skip the forwarding. ```go title="registrations/service.go" func (s *Service) Register(ctx context.Context, in Registration) error { tx, err := s.db.Begin(ctx) if err != nil { return err } // A no-op once the transaction has been committed. defer tx.Rollback(ctx) if _, err := tx.Exec(ctx, `INSERT INTO registrations (user_id, course_id) VALUES ($1, $2)`, in.UserID, in.CourseID, ); err != nil { return err } if _, err := tx.Exec(ctx, `INSERT INTO emails (recipients, subject, body) VALUES ($1, $2, $3)`, []string{in.Email}, "You are registered", renderConfirmation(in), ); err != nil { return err } // Both rows exist, or neither does. return tx.Commit(ctx) } ``` There is no window in which one happened and the other did not. ### The naive queue, and why it breaks [#the-naive-queue-and-why-it-breaks] A table of pending work is the easy part. The hard part is two workers. The obvious way to take a message is to read one and then mark it: ```sql title="the obvious version, which is wrong" SELECT id FROM emails WHERE status = 'queued' ORDER BY created_at LIMIT 1; -- then, with the id that came back: UPDATE emails SET status = 'sending' WHERE id = $1; ``` With one worker this is fine. With two, it sends duplicates, because nothing stops both from reading the same row before either has marked it. | Step | Worker A | Worker B | | ---- | ------------------------------------- | ------------------------------------------ | | 1 | Reads the oldest queued row, number 7 | | | 2 | | Reads the oldest queued row, also number 7 | | 3 | Marks 7 as sending | | | 4 | | Marks 7 as sending, which also succeeds | | 5 | Sends the email | Sends the same email | The textbook fix is to lock the row as you read it, by adding `FOR UPDATE` to the select. That removes the duplicate and creates a different problem. Worker B now waits at step 2 until A's transaction ends. When it does, B looks at row 7 again, finds it is no longer queued, and comes back with nothing, even though other messages are sitting right behind it. The workers end up taking turns. Adding more of them adds no speed. What you want is for B to ignore the row A is holding and take the next one. ### The solution: claiming work with SKIP LOCKED [#the-solution-claiming-work-with-skip-locked] This one statement is what makes Postgres usable as a queue. ```sql title="queries/emails.sql" UPDATE emails SET status = 'sending', attempts = attempts + 1, claimed_at = now() WHERE id IN ( SELECT id FROM emails WHERE status = 'queued' AND attempts < $1 ORDER BY created_at LIMIT $2 FOR UPDATE SKIP LOCKED ) RETURNING id, recipients, subject, body, attempts; ``` `FOR UPDATE` locks the rows the inner query selects. Without `SKIP LOCKED`, a second worker running the same query would wait for the first one's locks, then find the rows already claimed. With it, the second worker steps over locked rows and takes the next ones. Several workers can run at once and each message goes to exactly one of them. The claim and the status change happen in one statement, so there is no gap in which a row is selected but not yet marked. Notice what this does not do. It does not hold a transaction open while the email is being sent. The claim commits at once and the row is marked `sending`. A long transaction around slow network work would pin a connection and hold back vacuum for the whole table, which is the classic way a Postgres queue gets into trouble. ### The worker loop [#the-worker-loop] ```go title="email/worker.go" func (s *Service) RunWorker(ctx context.Context, cfg WorkerConfig) { ticker := time.NewTicker(cfg.Interval) defer ticker.Stop() for { // Recover anything a dead worker left behind before taking new work. if n, err := s.reclaimStale(ctx); err != nil { s.log.Error("reclaim failed", "err", err) } else if n > 0 { s.log.Warn("requeued abandoned email", "count", n) } // Keep going while full batches come back, so a backlog drains at the // speed of the relay and not one batch per tick. for { claimed, err := s.deliverBatch(ctx, cfg.BatchSize) if err != nil { s.log.Error("sweep failed", "err", err) break } if claimed < cfg.BatchSize { break } } select { case <-ctx.Done(): return case <-ticker.C: } } } ``` It polls. The interval only decides how quickly a new message is noticed on an idle system, because a backlog is drained in a tight inner loop. For email, fifteen seconds is nothing. ### Waking the worker at once with LISTEN and NOTIFY [#waking-the-worker-at-once-with-listen-and-notify] Polling was enough for my case, so I did not build this. If you need a message picked up immediately, Postgres can signal the worker. The producer adds one statement to its transaction. The notification is only delivered if the transaction commits. ```sql title="queries/emails.sql" SELECT pg_notify('email_queued', ''); ``` The worker holds one connection open and waits on it, with the polling interval as a fallback. ```go title="email/listener.go" func (s *Service) waitForWork(ctx context.Context, conn *pgx.Conn, interval time.Duration) { wait, cancel := context.WithTimeout(ctx, interval) defer cancel() // Returns when a notification arrives or the interval passes. Either way // the caller sweeps next, so an error here needs no handling. _, _ = conn.WaitForNotification(wait) } ``` Keep the fallback. A notification is not stored. If no worker is listening when it is sent, it is gone. That is fine here because the row is still in the table, and the next poll finds it. The notification is a hint to hurry, never the record of the work. ### When a worker dies mid-delivery [#when-a-worker-dies-mid-delivery] A row is `sending` while a worker holds it. If that worker is killed, the row stays `sending` for ever, and no claim query will ever pick it up. So each pass starts by returning old claims to the queue. ```sql title="queries/emails.sql" UPDATE emails SET status = 'queued', claimed_at = NULL WHERE status = 'sending' AND claimed_at < now() - make_interval(secs => $1); ``` The timeout is a real trade-off, so choose it on purpose. A message requeued while its first delivery was only slow gets sent twice. A message never requeued is never sent. A duplicate notice is a much smaller harm than a verification email that never arrives, so I set the timeout to five minutes, well above the thirty second SMTP timeout, and accept the remaining risk. If you have used Amazon SQS this will look familiar. It is the same idea as the SQS visibility timeout: a received message is hidden from other consumers for a while, and reappears if nobody deletes it in time. ### Recording the outcome during shutdown [#recording-the-outcome-during-shutdown] ```go title="email/worker.go" func (s *Service) deliverBatch(ctx context.Context, n int) (int, error) { batch, err := s.claim(ctx, n, maxAttempts) if err != nil { return 0, err } for _, msg := range batch { sendErr := s.mailer.Send(ctx, msg) // The outcome is written on its own context. Shutdown cancels ctx, and // a message delivered a moment before that would otherwise fail to // record its own success and be sent again on the next boot. outcome, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) if sendErr == nil { s.markSent(outcome, msg.ID) } else { // Back to 'queued' for another try, or 'failed' once the attempts // are spent, so a typo in an address cannot spin the queue for ever. s.markFailed(outcome, msg.ID, sendErr, msg.Attempts >= maxAttempts) } cancel() } return len(batch), nil } ``` This is the same `WithoutCancel` idea as the cache, for the same reason. The side effect has already happened, so the record of it must not depend on a context that may be cancelled at that moment. The attempts cap matters as much as the retry. Without it, one permanently undeliverable address is retried for ever and every sweep spends time on it. ### What delivery guarantee is this? [#what-delivery-guarantee-is-this] Every messaging system makes one of three promises. It is worth knowing which one you have. | Guarantee | Meaning | What goes wrong | Typical cause | | ------------- | ---------------------------------------- | ------------------------------- | ------------------------------------------------ | | At most once | A message is delivered zero or one times | Messages can be lost | Acknowledging before the work is done | | At least once | A message is delivered one or more times | Messages can be duplicated | Acknowledging after the work, with retries | | Exactly once | The effect of a message happens once | Hard to achieve, and never free | Needs idempotent work or transactions end to end | The queue in this post is **at least once**. A crash between sending the email and recording the outcome means the email is sent again after the reclaim timeout. No system gives you exactly once delivery to an outside service such as an SMTP relay, because the broker cannot make the relay's send and its own acknowledgement one atomic step. What systems call exactly once is really at least once delivery combined with work that is safe to repeat. So if a duplicate would be dangerous, make the handler idempotent, for example by recording a unique key for each message and refusing to process the same key twice. ### The same ideas under different names [#the-same-ideas-under-different-names] Dedicated brokers have names for everything this table does by hand. The mapping is close to one to one. | Concept | In a broker | In this table | | --------------------------------- | --------------------------------------------------- | -------------------------------------------- | | Take a message | Receive, consume, pop | The claim query with `SKIP LOCKED` | | Confirm it is done | Acknowledge, delete, commit offset | Set `status` to `sent` | | Hide it while it is processed | Visibility timeout (SQS), unacknowledged (RabbitMQ) | `status = 'sending'` with `claimed_at` | | Return it if the worker died | Redelivery, `XAUTOCLAIM` in Redis Streams | The reclaim sweep | | Park it after too many failures | Dead letter queue or exchange | `status = 'failed'` once attempts are spent | | Several workers, one message each | Competing consumers, a consumer group | Several workers running the same claim query | | How far behind are we | Queue depth, consumer lag | `count(*)` where status is `queued` | ### Postgres against the brokers [#postgres-against-the-brokers] | Question | Postgres table | RabbitMQ | Amazon SQS | Apache Kafka | Redis Streams | NATS JetStream | | ----------------------------- | ---------------------- | --------------------------- | ------------------------------------ | ----------------------------- | ------------------------- | -------------------------- | | Family | Queue | Queue | Queue | Log | Log | Log | | Default guarantee | At least once | At least once with acks | At least once | At least once | At least once with groups | At least once | | Ordering | Roughly by insert time | Per queue | Best effort, strict with FIFO queues | Strict within a partition | Strict within a stream | Strict within a stream | | Replay old messages | Only if you keep rows | Not on classic queues | No | Yes, for the retention period | Yes | Yes | | Many independent readers | Build it yourself | Yes, through exchanges | Through SNS fan-out | Yes, consumer groups | Yes, consumer groups | Yes, consumers | | Routing rules | Any SQL `WHERE` | Rich: direct, topic, fanout | None | By topic and partition key | By stream | By subject, with wildcards | | Same transaction as your data | Yes | No | No | No | No | No | | Throughput ceiling | Modest | High | High, and managed for you | Very high | High | Very high | | What you operate | Nothing new | A broker, often a cluster | Nothing, it is a service | A cluster of brokers | Redis | A NATS server or cluster | A few of those rows deserve a sentence. **Ordering.** With one worker, this table is processed in insert order. With several, two workers can finish out of order, and a retried message jumps behind newer ones. Email does not care. A stream of account balance changes does. If order matters, Kafka's rule is the one to copy: everything that must stay in order shares a key and is handled by one consumer. **Replay.** Kafka keeps messages after they are read, so a new service can start from the beginning and catch up on history. That is the defining feature of a log, and it is why Kafka is used for event sourcing, analytics pipelines and feeding several systems from one stream. A queue table has nothing like it unless you add a position per reader, at which point you are building a small Kafka badly. **Fan-out.** In this table a message has one status, so it belongs to one consumer. Delivering the same event to five independent services needs five rows or a second table of per-consumer positions. RabbitMQ exchanges, Kafka consumer groups and NATS subjects do this out of the box. **Throughput.** Each message here costs an insert, an update to claim it, and an update to finish it, all through the write-ahead log, and each leaves dead row versions for vacuum. That is comfortable at tens or hundreds of messages a second on ordinary hardware. A system built as an append-only log does far less work per message and scales much further. ### Advantages of queuing in Postgres [#advantages-of-queuing-in-postgres] * **Atomic with your data.** A message is enqueued if and only if the transaction commits. This removes a whole class of bugs. * **Nothing new to operate.** No broker cluster, no client library with its own failure modes, no extra credentials or network rules. * **Durable by default.** Rows go through the same write-ahead log, replication and backups as everything else you store. * **Fully inspectable.** Finding a stuck message is a `SELECT`. Retrying a failed one is an `UPDATE`. Most brokers make both of those harder than they should be. * **The history is free.** The table is already an audit trail of what was sent, to whom, when, and with what error. * **Flexible selection.** Priority, scheduled delivery and per-tenant fairness are an extra column and a different `ORDER BY` or `WHERE`. * **Safe with many workers.** `SKIP LOCKED` gives competing consumers without any coordination service. ### Disadvantages of queuing in Postgres [#disadvantages-of-queuing-in-postgres] * **A lower ceiling.** It will not carry the message volumes Kafka or NATS are built for. * **Table churn.** Every status change is a new row version. Busy queues need vacuum to keep up, and long-running transactions elsewhere in the database can stop it from doing so. * **Polling adds latency.** Without `LISTEN` and `NOTIFY`, a message waits up to one interval before it is seen. * **No replay, no fan-out, no routing.** These have to be built, and each one is real work. * **It uses your database's resources.** Workers hold connections, and a backlog adds load to the system that serves your users. * **You own the edge cases.** Reclaim timeouts, retry caps, back-off and poison messages are code you write and test. A broker ships them. * **It only suits one database.** Services that do not share the database cannot share the queue. ### Why a queue table needs looking after [#why-a-queue-table-needs-looking-after] The warnings above about dead rows and vacuum come from how Postgres stores changes, and it is worth understanding because it explains every piece of advice that follows. Postgres never overwrites a row in place. An `UPDATE` writes a complete new version of the row and marks the old version as dead. A `DELETE` only marks the row dead. The dead versions stay on disk until a background process called **vacuum** confirms that no running transaction could still need to see them, and then frees the space for reuse. This design is called multi-version concurrency control, and it is what lets readers and writers work at the same time without blocking each other. A queue is close to a worst case for it. Each message is inserted once and updated at least twice, so a queue that has handled a million messages has produced at least two million dead row versions. Normally vacuum clears them as fast as they appear and nobody notices. The trouble starts when vacuum cannot keep up, and the usual reason is one long-running transaction somewhere in the database. Vacuum may not remove any row version that an open transaction might still be able to see. A report that runs for an hour, or a transaction left open by a forgotten session, therefore pins every dead row created since it began. The table and its indexes swell, the claim query has to step over more and more dead entries to find a live one, and the queue slows down for a reason that has nothing to do with the queue. That is the background to three small habits. Watch it. One query gives you depth and age, which are the two numbers that matter. ```sql title="queries/queue_health.sql" SELECT status, count(*) AS messages, now() - min(created_at) AS oldest FROM emails WHERE status IN ('queued', 'sending', 'failed') GROUP BY status; ``` A growing `queued` count means workers are not keeping up. An old `sending` row means the reclaim sweep is not running. A rising `failed` count means something downstream is rejecting messages. Keep transactions short. Claim, commit, do the work, then record the outcome. Never hold a transaction open across a network call. Decide what happens to finished rows. I keep them because they are the audit trail, and the partial indexes mean they cost nothing at claim time. If you do not need the history, delete or archive completed rows on a schedule so the table does not grow without bound. ### You do not have to write this yourself [#you-do-not-have-to-write-this-yourself] I wrote it by hand because the need was small and the rows already existed. If you want more features, several mature projects use exactly this approach, across most languages: * **River** is a job queue for Go built on Postgres, with retries, scheduling, unique jobs and a web interface. * **pgmq** is a Postgres extension that gives you an SQS-style queue as SQL functions. * **Oban** for Elixir, **pg-boss** for Node.js, **Que** for Ruby and **Solid Queue** for Rails all follow the same design. The fact that so many ecosystems have arrived at the same place is a good sign that the idea is sound. ## The same trick, a third time [#the-same-trick-a-third-time] Once the pattern is in place it keeps paying off. Rate limiting across several API instances needs a shared counter, which is usually another reason to add Redis, where you would pair `INCR` with `EXPIRE`. Here it is one upsert on another unlogged table. ```sql title="queries/rate_limits.sql" INSERT INTO rate_limits (key, window_start, hits, expires_at) VALUES ($1, $2, 1, $3) ON CONFLICT (key, window_start) DO UPDATE SET hits = rate_limits.hits + 1 RETURNING hits; ``` ```go title="ratelimit/limiter.go" func (l *Limiter) Allow(ctx context.Context, key string) bool { // Truncating to the window means every instance agrees on the bucket // boundary without talking to each other. start := time.Now().Truncate(l.window) var hits int err := l.db.QueryRow(ctx, incrementSQL, key, start, start.Add(l.window)).Scan(&hits) if err != nil { // Fail open. A limiter is a safeguard, not a reason to take the API // down when the database hiccups. return true } return hits <= l.limit } ``` Concurrent requests, including ones on other instances, queue up on the row, so two of them can never read the same stale count. This is a fixed window, so a caller can make up to twice the limit across a window boundary: a full allowance at the end of one minute and another at the start of the next. That is the accepted price for a single round trip and no extra bookkeeping. A sliding window is more accurate and costs more per request. ## None of this is specific to Go [#none-of-this-is-specific-to-go] Everything above that matters is either SQL or an idea. The SQL runs unchanged from any language. The ideas each have a direct equivalent wherever you work. | The idea | In the Go examples | Elsewhere | | ------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | The cache table, upsert, expiry check and prefix delete | SQL sent through pgx | The same SQL through node-postgres, psycopg, JDBC, Npgsql or any other driver | | Concurrent misses on one key share a single load | `singleflight.Group` | Keep the in-flight promise, future or task in a map keyed by cache key, and hand that same one to every caller | | Bookkeeping finishes even if the client disconnects | `context.WithoutCancel` with a timeout | Do not tie the write to the request's abort signal or cancellation token, and give it its own short timeout | | The claim query and the reclaim sweep | SQL sent through pgx | The same SQL. `FOR UPDATE SKIP LOCKED` is a feature of Postgres, not of the client | | A worker that sweeps on an interval | A goroutine with a `time.Ticker` | A background task, a timer loop, a separate worker process, or a scheduled job | | Enqueue with the data, atomically | `pool.Begin`, two inserts, `Commit` | Any transaction API. Both inserts go inside one transaction | | Stop cleanly on shutdown | A cancelled context ends the loop | Handle the termination signal, stop claiming, and let in-flight work record its outcome | Two of those are worth a closer look because they are the ones most often got wrong in any language. **Sharing one load per key.** The mistake is to cache only the finished value. Between the first miss and the value arriving, every other caller also sees a miss. The fix is to cache the work in progress as well. The first caller starts the load and stores the pending result. Everyone who arrives before it finishes is handed that same pending result and waits on it. When it resolves, it is removed from the map. That is all `singleflight` does, and it is a dozen lines in most languages. **Finishing after the client has gone.** Many frameworks cancel work when the client disconnects, which is right for reads and wrong for cleanup. Look at how your framework propagates cancellation, and make sure cache invalidation and "this message was delivered" writes are not on that path. ## How to decide [#how-to-decide] ### Postgres is the right call when [#postgres-is-the-right-call-when] * You already run Postgres and would be adding a service only for this. * Message volume is modest, and a delay of a few seconds is acceptable. * The work is naturally tied to a database write, so enqueueing in the same transaction has real value. * The queued items are data you would store anyway. * The cached endpoints are slow to compute, so a SQL round trip is still a large saving. * The team is small and every extra piece of infrastructure has a real cost in attention. ### Reach for a dedicated tool when [#reach-for-a-dedicated-tool-when] | If you need | Use | | ---------------------------------------------------------------- | ------------------------------ | | Reads in well under a millisecond, thousands of times a second | Redis or Memcached | | A cache with a hard memory limit and automatic eviction | Redis or Memcached | | Sorted sets, counters, leaderboards, sessions, distributed locks | Redis | | The simplest possible cache with nothing else attached | Memcached | | Complex routing between many producers and consumers | RabbitMQ | | A queue with no servers to manage at all | Amazon SQS | | An event stream that several services read independently | Apache Kafka | | To replay history, or rebuild state from past events | Apache Kafka | | Very high throughput across many machines | Apache Kafka or NATS JetStream | | Low-latency messaging between many small services | NATS | | Streams, and you already run Redis | Redis Streams | ### Signs you have outgrown it [#signs-you-have-outgrown-it] * Cache reads show up near the top of your slowest or most frequent queries. * The connection pool runs out under load and cache or worker traffic is a large share of it. * The queue table keeps growing in size even though the number of live messages is steady. * You find yourself adding a per-consumer position column, or copying messages so a second service can read them. * Other teams or services want to consume the same messages. None of these is a failure. They are the point at which the extra infrastructure starts paying for itself, and moving is straightforward because the interface is small. The cache is four methods. The queue is enqueue, claim, and record an outcome. Both can be reimplemented on Redis or a broker without touching the code that calls them. ## Summary [#summary] * An **unlogged table** is a perfectly good cache when the thing being cached is slow to compute. It has no eviction, so sweep it. * **Share one load per key** to stop a stampede when an entry expires, whatever the cache is built on. In Go that is `singleflight`. * **Detach invalidation and outcome writes from request cancellation**, or a disconnect will leave stale state behind. * **`FOR UPDATE SKIP LOCKED`** turns a table into a queue that several workers can share safely, from any language. * Postgres never updates a row in place, so **keep transactions short** and let vacuum do its job. * Enqueueing **in the same transaction** as the data is the advantage no broker can offer. * The result is **at least once**. Make handlers safe to repeat. * Postgres is a good queue and a poor log. If you need replay, fan-out or very high throughput, use the tool built for it. One database you already run, back up and understand is a real feature. The whole thing here is two tables, a few hundred lines of application code, and nothing new to operate. Start there, measure, and add the dedicated tool on the day the numbers ask for it. # Mermaid ## Setup [#setup] Install the required dependencies, `next-themes` is used to manage the light/dark mode. npm pnpm yarn bun ```bash npm install mermaid next-themes ``` ```bash pnpm add mermaid next-themes ``` ```bash yarn add mermaid next-themes ``` ```bash bun add mermaid next-themes ``` Create the Mermaid component: ```tsx title="mermaid.tsx" 'use client'; import { useEffect, useId, useRef, useState } from 'react'; import { useTheme } from 'next-themes'; export function Mermaid({ chart }: { chart: string }) { const id = useId(); const [svg, setSvg] = useState(''); const containerRef = useRef(null); const currentChartRef = useRef(null); const { resolvedTheme } = useTheme(); useEffect(() => { const container = containerRef.current; if (currentChartRef.current === chart || !container) return; currentChartRef.current = chart; async function renderChart() { const { default: mermaid } = await import('mermaid'); try { const isDark = resolvedTheme === 'dark'; mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', fontFamily: 'inherit', themeCSS: ` margin: 1.5rem auto 0; ${ isDark ? ` .node rect, .node circle, .node ellipse, .node polygon, .node path { fill: #1f2937 !important; stroke: #ff9c08 !important; stroke-width: 2px !important; } .node .label { color: #f3f4f6 !important; } .edgeLabel { background-color: #1f2937 !important; color: #f3f4f6 !important; } .edgePath .path { stroke: #ff9c08 !important; stroke-width: 2px !important; } .cluster rect { fill: #1f2937 !important; stroke: #ff4e32 !important; stroke-width: 2px !important; } .section { stroke: #ff4e32 !important; } ` : '' } `, theme: isDark ? 'dark' : 'default' }); const { svg, bindFunctions } = await mermaid.render(id, chart.replace(/\\n/g, '\n')); bindFunctions?.(container!); setSvg(svg); } catch (error) { console.error('Error while rendering mermaid', error); } } void renderChart(); }, [chart, id, resolvedTheme]); return (
); } ``` > This is originally inspired by [remark-mermaid](https://github.com/the-guild-org/docs/blob/main/packages/remark-mermaid/src/mermaid.tsx). Add the component as a MDX component: ```tsx title="mdx-components.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; import { Mermaid } from '@/components/shared/mermaid'; export function getMDXComponents(components?: MDXComponents): MDXComponents { return { Mermaid, ...components }; } ``` ## Usage [#usage] Use it in MDX files. ```mdx ``` # Tab Animation with motion/react ## Requirements [#requirements] Tabs are a common UI element that can be used to switch between different views. In this post, we will create a simple tab animation with motion/react. This post assumes that you have a basic understanding of React. If you are new to React, you can follow the [React getting started guide](https://react.dev/). We will also use [tailwindcss v4](https://tailwindcss.com/) for styling and [motion/react](https://www.framer.com/motion/) a popular animation library for React. ## Setup [#setup] We will install the required dependencies for the project. npm pnpm yarn bun ```bash npm install motion/react tailwindcss@next @tailwindcss/postcss postcss autoprefixer clsx ``` ```bash pnpm add motion/react tailwindcss@next @tailwindcss/postcss postcss autoprefixer clsx ``` ```bash yarn add motion/react tailwindcss@next @tailwindcss/postcss postcss autoprefixer clsx ``` ```bash bun add motion/react tailwindcss@next @tailwindcss/postcss postcss autoprefixer clsx ``` ## Styling the Tabs [#styling-the-tabs] We will use Tailwind CSS v4 to style the tabs. In v4, configuration is now done using CSS instead of JavaScript config files. ### PostCSS Configuration [#postcss-configuration] Create a `postcss.config.js` file in the root of your project: ```js title="postcss.config.js" module.exports = { plugins: { '@tailwindcss/postcss': {}, autoprefixer: {} } }; ``` ### Global CSS Configuration [#global-css-configuration] In Tailwind CSS v4, configuration is now done using CSS instead of JavaScript config files. Create or update your `app/globals.css` file: ```css title="app/globals.css" @import 'tailwindcss'; /* Customize your theme using CSS variables */ @theme { --color-orange-300: #fdba74; --color-orange-300-dark: #ea580c; } ``` This CSS-based configuration approach is much simpler than the old JavaScript config. You can define custom colors, spacing, and other design tokens directly in your CSS using the `@theme` directive. ## Animating the Tabs [#animating-the-tabs] We will use the `AnimatePresence` component from motion/react to animate the tabs. We will also use the `motion` component to animate the tab pointer. ## Creating the Tabs [#creating-the-tabs] ### Data for the components [#data-for-the-components] We will create a simple array that will hold the tab data for the component. The data will contain the tab title and content. ```ts title="data.ts" export const tabs = [ { title: 'Frontend', content: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum voluptas ducimus.' }, { title: 'Backend', content: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.' } ]; ``` ### Tab component [#tab-component] A new file `Tab.tsx` will be created in the `components` folder. This component will take the tab data as props and render the tab title and content. ```tsx title="components/animated-tab.tsx" 'use client'; import { useId, useState } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { cn } from 'lib/utils'; interface TabProps { title: string; content: string; } export default function Tab({ contains, className }: { contains: TabProps[]; className?: string }) { const [tab, setTab] = useState(contains[0]); const animationId = useId(); return (
{contains.map((item, idx) => ( ))}
{contains.map( (item) => item.content === tab.content && ( {item.content} ) )}
); } ``` ### Using the Tab component [#using-the-tab-component] We will import the `Tab.tsx` component into your component or page example `app/page.tsx` file and pass the `tabs` data as props. ```tsx title="app/page.tsx" import Tab from '~/components/animated-tab'; import { tabs } from '~/data'; export default function Home() { return (
); } ``` ## Conclusion [#conclusion] We have successfully created a tab component with motion/react and Tailwind CSS v4. The new CSS-based configuration using `@import "tailwindcss"` and the `@theme` directive makes setup much simpler than the old JavaScript config approach. Our tab component can be used in any project to display content in a tabbed format, is responsive, and works on any device. Below is the result of the tab component we created. # The URL Is a State Manager, and Filters Do Not Belong in useState ## A bug you have seen before [#a-bug-you-have-seen-before] You are on a product list. You filter by category, sort by price, and click through to page three. Then one of these happens: * You refresh, and you are back on page one with no filter. * You copy the address to a colleague, and they see the unfiltered list. * You open a product, press back, and the list has forgotten everything. * You open the same page in a second tab to compare, and it starts from scratch. None of these is a bug in the usual sense. The code does what it was written to do. The filter, the sort order and the page number were stored in component state, and component state lives in the memory of one tab for as long as that component is mounted. Refresh, navigate away, or open a new tab, and it is gone. This is the same in every framework, and with no framework at all. In React it is `useState`. In Vue it is `ref`. In Angular it is a `signal`. In Svelte it is a `$state` rune. In plain JavaScript it is a variable. The names differ and the problem is identical. ## Where state can live [#where-state-can-live] The fix starts with a question that is easy to skip: where should this piece of state live? There are more options than "in the component". | Where | Survives a reload | Can be shared as a link | Works with back and forward | Separate per tab | Good for | | --------------- | ----------------- | ----------------------- | --------------------------- | ---------------- | --------------------------------------------------- | | Component state | No | No | No | Yes | A menu being open, a field being typed in | | A global store | No | No | No | Yes | State many components share for one session | | Local storage | Yes | No | No | No, it is shared | Preferences such as a theme or a dismissed banner | | The URL | Yes | Yes | Yes | Yes | What the visitor is looking at: filters, sort, page | | The server | Yes | To anyone with access | Not applicable | No, it is shared | Data that belongs to the user or the business | By "component state" I mean `useState` or `useReducer` in React, `ref` or `reactive` in Vue, a `signal` in Angular and `$state` in Svelte. By "a global store" I mean Redux, Zustand, Pinia, NgRx and their relatives. A global store fixes one thing that component state cannot do, which is sharing between distant components. It does not fix any of the four problems in the list above, because it is still memory in one tab. Only one row has "yes" all the way across the first three columns. ## What the URL gives you for free [#what-the-url-gives-you-for-free] When state is in the URL, the browser does work you would otherwise have to write. * **Reload restores it.** The address is the first thing the browser has on a reload. * **Links carry it.** Anyone who opens the link sees what you saw. * **Back and forward step through it.** The browser's history is an undo stack you did not have to build. * **Bookmarks keep it.** A visitor can save a filtered view and come back to it next week. * **Each tab has its own.** Two tabs can show two different filters without fighting. * **The server can see it.** With server rendering, the first response can already be filtered and sorted, with no flash of the default view. * **It is inspectable.** When a user reports a problem, the address they paste tells you exactly what they were looking at. The mental model that makes this click: the URL is the state, written down. The page is a function of it. Controls do not hold a value. They read it from the URL and write changes back to it. ## The usual version, and what is wrong with it [#the-usual-version-and-what-is-wrong-with-it] Here is a list with a category filter and pagination, written the way most of us write it first. Pick your framework. The tabs stay in step for the rest of the post, so you only have to choose once. ```ts title="plain variables" let category = 'all'; let page = 1; function render() { const visible = paginate(filterBy(products, category), page); list.replaceChildren(...visible.map(productCard)); } picker.addEventListener('change', () => { category = picker.value; page = 1; render(); }); pager.addEventListener('click', (event) => { page = Number((event.target as HTMLElement).dataset.page); render(); }); ``` ```tsx title="useState" function ProductList({ products }: { products: Product[] }) { const [category, setCategory] = useState('all'); const [page, setPage] = useState(1); const visible = paginate(filterBy(products, category), page); return ( <> ); } ``` ```vue title="ref" ``` ```ts title="signal" @Component({ selector: 'app-product-list', template: ` ` }) export class ProductListComponent { products = input.required(); category = signal('all'); page = signal(1); visible = computed(() => paginate(filterBy(this.products(), this.category()), this.page())); } ``` ```svelte title="$state" ``` Every one of these is clean and works. Every one of them has all four problems from the top of this post, because in each the two values are held in memory. The address bar says `/products` whatever the visitor is looking at. ## Moving it to the URL by hand [#moving-it-to-the-url-by-hand] No library is required in any of them. The pattern does not change from tab to tab: **read the value from the URL, and write a change by navigating to a new URL.** This is what every framework is doing underneath. ```ts title="URLSearchParams and the History API" function readPage(): number { const params = new URLSearchParams(window.location.search); return Number(params.get('page')) || 1; } function writePage(page: number) { const params = new URLSearchParams(window.location.search); // The default is left out, so page one keeps a clean address. if (page === 1) params.delete('page'); else params.set('page', String(page)); const query = params.toString(); history.pushState(null, '', query ? `?${query}` : window.location.pathname); render(); } // Back and forward change the URL without loading a page. Listen for it. window.addEventListener('popstate', render); ``` `render` now calls `readPage()` where it used to read the `page` variable. There is no variable any more. React has no router of its own, so this depends on yours. In the Next.js App Router it looks like this. ```tsx title="Next.js navigation hooks" 'use client'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; function usePage(): [number, (page: number) => void] { const searchParams = useSearchParams(); const pathname = usePathname(); const router = useRouter(); const page = Number(searchParams.get('page')) || 1; const setPage = (next: number) => { const params = new URLSearchParams(searchParams); // The default is left out, so page one keeps a clean address. if (next === 1) params.delete('page'); else params.set('page', String(next)); const query = params.toString(); router.push(query ? `${pathname}?${query}` : pathname); }; return [page, setPage]; } ``` It returns the same pair `useState` does, so swapping it in changes one line of the component. Vue Router exposes the query string as a reactive object, so a writable computed is all it takes. ```vue title="Vue Router" ``` `page` still works with `v-model`. The component using it cannot tell that the value now lives in the address bar. The router gives you the query string as an observable. Turned into a signal, it reads just like the `signal` it replaces. ```ts title="ActivatedRoute and Router" export class ProductListComponent { private route = inject(ActivatedRoute); private router = inject(Router); private params = toSignal(this.route.queryParamMap, { requireSync: true }); page = computed(() => Number(this.params().get('page')) || 1); setPage(page: number) { this.router.navigate([], { relativeTo: this.route, // null removes the key, so page one keeps a clean address. queryParams: { page: page === 1 ? null : page }, // Keep every other parameter that is already in the URL. queryParamsHandling: 'merge' }); } } ``` The template still calls `page()`. Only the place the value comes from has changed. In SvelteKit the current URL is reactive state, and `goto` navigates. ```svelte title="SvelteKit" ``` `current` is derived from the URL, so it updates on back and forward with no extra code. Compare the tabs and the shape is identical. Parse a string from the query, fall back to a default when it is missing, leave the default out when writing, keep the other parameters, and navigate. Only the names of the router functions differ. Written out like this, the repeated work is also obvious. Do it for five parameters and you have written a small library. That is the reason to reach for one. ## The same thing with less code [#the-same-thing-with-less-code] Each ecosystem has a shorter way to say the same thing. There is nothing to install. The two functions from the last section are the library. If you have several parameters, turn them into one small helper that takes a name, a default and a parser, and you have written the core of every library in the other tabs. ```ts title="one helper for every parameter" function queryParam(name: string, fallback: T, parse: (raw: string) => T) { return { read(): T { const raw = new URLSearchParams(window.location.search).get(name); return raw === null ? fallback : parse(raw); }, write(value: T) { const params = new URLSearchParams(window.location.search); if (value === fallback) params.delete(name); else params.set(name, String(value)); const query = params.toString(); history.pushState(null, '', query ? `?${query}` : window.location.pathname); } }; } const page = queryParam('page', 1, Number); ``` In React I use [nuqs](https://nuqs.dev). It gives you a hook with the shape of `useState`, typed parsers, and sensible handling of defaults. The archive on [this blog's index](/blog) keeps its topic filter and its page number this way. ```tsx title="src/app/blog/_components/post-archive.tsx" const [tagsParam, setTags] = useQueryState( 'tags', parseAsArrayOf(parseAsString).withDefault([]).withOptions({ clearOnDefault: true }) ); const [pageParam, setPage] = useQueryState( 'page', parseAsInteger.withDefault(1).withOptions({ clearOnDefault: true }) ); ``` The parser turns the string in the URL into a typed value and back. `parseAsInteger` means `pageParam` is a `number`, not `"2"`. `parseAsArrayOf(parseAsString)` means `tagsParam` is a `string[]`, written to the URL as `?tags=react,golang`. * `withDefault` gives the value used when the parameter is missing, and removes `null` from the type. * `clearOnDefault` removes the parameter when the value equals the default, so page one of the unfiltered archive is `/blog`, not `/blog?tags=&page=1`. [VueUse](https://vueuse.org/router/useRouteQuery/) has `useRouteQuery`, which is the writable computed from the last section in one call. ```ts title="@vueuse/router" import { useRouteQuery } from '@vueuse/router'; // A ref. Reading it reads the URL, and assigning to it navigates. const page = useRouteQuery('page', '1', { transform: Number }); ``` The second argument is the default, and `transform` turns the string from the URL into the type you want. `page.value = 2` updates the address bar. Angular needs no extra package. Turn on component input binding and the router fills a component input from the query parameter of the same name. ```ts title="app.config.ts" export const appConfig: ApplicationConfig = { providers: [provideRouter(routes, withComponentInputBinding())] }; ``` ```ts title="product-list.component.ts" export class ProductListComponent { // Filled from ?page= by the router. Missing or invalid becomes 1. page = input(1, { transform: (value: unknown) => Number(value) || 1 }); } ``` That covers reading. Writing is still `router.navigate` with `queryParamsHandling: 'merge'`, as in the last section. SvelteKit already gives you the reactive half. `page.url.searchParams` updates on every navigation, so reading needs no library at all. What is worth extracting is the write, into one function you reuse for every parameter. ```ts title="src/lib/query.ts" import { goto } from '$app/navigation'; import { page } from '$app/state'; export function setQuery(name: string, value: string | null) { const url = new URL(page.url); if (value === null) url.searchParams.delete(name); else url.searchParams.set(name, value); return goto(url, { keepFocus: true, noScroll: true }); } ``` Whichever tab you are on, try the result on a real page. Open the [blog index](/blog), pick two topics, go to page two, and copy the address. That link opens the same view for anyone. ## Four details that apply in every framework [#four-details-that-apply-in-every-framework] Getting a value into the URL is the easy half. These are the parts that make it hold up, and none of them depends on the library or the framework. ### The URL is user input [#the-url-is-user-input] This is the one that is easiest to forget. With `useState`, your own code is the only thing that ever sets the value, so it is always valid. Once the value is in the address bar, anyone can type anything. A link can be a year old. A category can have been renamed. So what comes out of the URL is not yet trustworthy. For the tags, the archive checks each one against the tags that actually exist. ```tsx const tags = [...new Set(posts.flatMap((post) => post.tags))].sort((a, b) => a.localeCompare(b)); // A topic no post carries is a stale or mistyped link, not a filter, and a // topic named twice is still one topic. const selected = [...new Set(tagsParam)].filter((tag) => tags.includes(tag)); ``` A visitor who arrives at `?tags=nonsense` sees the normal archive, not an empty list under a heading that says "Posts on nonsense". For the page, the number is clamped to the range that exists. ```tsx const pageCount = Math.max(1, Math.ceil(matching.length / PAGE_SIZE)); const page = Math.min(Math.max(pageParam, 1), pageCount); ``` `?page=999` shows the last page. `?page=-3` shows the first. Notice the naming: `tagsParam` and `pageParam` are what the URL said, and `selected` and `page` are what the component decided to believe. Everything after those lines uses the second pair. ### State that depends on other state [#state-that-depends-on-other-state] Choosing a topic changes how many results there are. If you were on page three of everything and pick a topic with four posts, page three no longer exists. So changing the filter also resets the page. ```tsx const toggleTag = (tag: string) => { setTags(selected.includes(tag) ? selected.filter((name) => name !== tag) : [...selected, tag]); setPage(1); }; ``` This is true with `useState` as well. It matters more here because a broken combination is now something a visitor can bookmark. ### Push or replace [#push-or-replace] Every change to the URL either adds a history entry or overwrites the current one, and you have to choose. | Choice | Back button | Use it for | | ------- | ----------------- | -------------------------------------------------------- | | Push | Undoes the change | Deliberate steps: opening a tab, going to another page | | Replace | Leaves the page | Rapid or minor changes: typing in a search box, a slider | If typing "shoes" in a search box pushes five entries, the visitor has to press back five times to leave. If changing page replaces, back skips the list entirely. Neither is wrong everywhere, so decide per parameter. Every router offers both. | Where | Push | Replace | | ------------------ | ---------------------------- | ----------------------------------------- | | Plain JavaScript | `history.pushState` | `history.replaceState` | | React with Next.js | `router.push` | `router.replace` | | React with nuqs | The `history: 'push'` option | The default | | Vue Router | `router.push` | `router.replace` | | Angular | `router.navigate` | `router.navigate` with `replaceUrl: true` | | SvelteKit | `goto` | `goto` with `replaceState: true` | ### Keep the address short and clean [#keep-the-address-short-and-clean] A URL is something people read, paste into chat and say out loud. * **Leave defaults out.** `/blog` is better than `/blog?page=1&sort=newest&tags=`. * **Throttle free text.** Update the address a few times a second at most, not on every keystroke. nuqs has a `throttleMs` option for this. * **Use short, stable names.** `?q=` and `?page=` will outlive a redesign. A parameter name is a public interface once people have bookmarked it. * **Never put secrets in it.** URLs are written to server logs, saved in browser history, and sent to other sites in the referrer header. Tokens, emails and anything personal do not belong there. ## What it changes about rendering [#what-it-changes-about-rendering] Moving state to the URL has one consequence that component state never had: the server may or may not know the value when it renders. In a server-rendered app this is a benefit. The request includes the query string, so the first response can already be filtered. There is nothing to "restore" on the client. In a statically generated app it is a constraint. The page is built before any visitor exists, so there is no query string to read. In the Next.js App Router, a component that reads search params must sit inside a `Suspense` boundary, and the build fails without one. Where you put it matters, because React gives up on static rendering at the nearest boundary. This blog wraps only the part that reads the URL. ```tsx title="src/app/blog/page.tsx" }> ``` The heading and the row of latest posts are in the static HTML that a crawler or a slow connection sees first. Only the archive waits for the client. One more thing that tripped me up. The archive rows animate in, and my first instinct was to reveal them as they scroll into view. That breaks when the URL changes the list under you: clicking "Older" creates new rows inside a list you are already looking at, and an in-view trigger can leave them invisible. The list is keyed by the view and animated directly, so a new filter or page mounts a new list and the entrance plays at once. ```tsx ``` ## When component state is the right answer [#when-component-state-is-the-right-answer] None of this means `useState` and `ref` are wrong. They are right for most state. The URL is for one particular kind. A good test: **if someone sent you this link, would you be annoyed that it did not restore this?** If yes, it belongs in the URL. If you would not even notice, keep it in the component. | State | Where it belongs | Why | | --------------------------------------------- | ------------------------ | ------------------------------------------------ | | Filters, sort order, search term, page number | The URL | It describes what is on screen | | The selected tab or the open record | The URL | People link to "the billing tab" | | A date range on a report | The URL | The first thing anyone does is share the report | | A dropdown or tooltip being open | Component state | It is a moment, not a view | | Text typed into a form but not submitted | Component state | Sharing half a sentence helps nobody | | Hover, focus, drag position | Component state | It changes far too often and means nothing later | | Theme, language, a dismissed banner | Local storage or cookies | It is about the person, not the page | | The signed-in user, a cart, saved items | The server | It must follow the user across devices | | A password, a token, personal details | Never the URL | URLs are logged, stored and shared | Two limits are worth knowing even for state that passes the test. * **Size.** A URL is not a database. Browsers and servers cap its length, and long before that it becomes unreadable. A handful of short values is fine. A whole form or a list of fifty selected ids is not. Store that on the server and put its id in the URL. * **Frequency.** Writing to the URL on every frame of a drag or every keystroke is wasteful and floods the history. Keep the fast-changing value in component state and write to the URL when the interaction settles. ## Summary [#summary] * Component state lives in one tab's memory. Reload, share, back and bookmark all lose it, in every framework. * A global store does not fix that. It is still memory in one tab. * The URL is the one place that survives a reload, can be shared, and works with the back button. * The pattern is the same in plain JavaScript, React, Vue, Angular and Svelte: read from the URL, and write by navigating. A library or a built-in router feature removes the repetition. * Treat what comes out of the URL as user input. Validate it and clamp it. * Reset dependent state, such as the page, when the thing it depends on changes. * Choose push or replace per parameter, leave defaults out, and never put secrets in a URL. * Keep moments in the component, preferences in storage, and the user's data on the server. # createPortal (React) ## Requirements [#requirements] React's `createPortal` API allows you to render elements outside of their parent component's DOM hierarchy. This is especially useful when dealing with UI elements like modals, tooltips, dropdowns, and overlays that must break out of the normal rendering flow. *** ## 📦 Requirements [#-requirements] To use `createPortal`, make sure your project meets these requirements: * React 16 or higher * `react-dom` installed * An additional DOM node (e.g. a `div#modal-root`) defined in your base HTML Install React and ReactDOM if you haven't already: npm pnpm yarn bun ```bash npm install react react-dom ``` ```bash pnpm add react react-dom ``` ```bash yarn add react react-dom ``` ```bash bun add react react-dom ``` Install type in-case of typescript. npm pnpm yarn bun ```bash npm install -D @types/react @types/react-dom ``` ```bash pnpm add -D @types/react @types/react-dom ``` ```bash yarn add --dev @types/react @types/react-dom ``` ```bash bun add --dev @types/react @types/react-dom ``` ## 🏠 HTML Setup [#-html-setup] First, add a portal target to your HTML: ```html title="index.html" My App
``` ## 🎯 Basic Portal Example [#-basic-portal-example] ```tsx title="modal.tsx" twoslash import * as React from 'react'; import { createPortal } from 'react-dom'; type ModalProps = { children: React.ReactNode; isOpen: boolean; onClose: () => void; }; export function Modal({ children, isOpen, onClose }: ModalProps) { React.useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); }, [onClose]); if (!isOpen) return null; const modalRoot = document.getElementById('modal-root'); if (!modalRoot) { console.error('Modal root element not found'); return null; } return createPortal(
e.stopPropagation()} > {children}
, modalRoot ); } ``` ```tsx title="App.tsx" import { useState } from 'react'; import { Modal } from './modal.tsx'; export default function App() { const [isOpen, setIsOpen] = useState(false); return (
This container has overflow hidden, but the modal escapes it!
setIsOpen(false)} >

Modal Title

This modal renders outside the main React root!

); } ``` ## 📝 Key Takeaways [#-key-takeaways] * Type Safety: Use TypeScript interfaces to define clear prop contracts * Error Handling: Always check if portal target exists and handle gracefully * Performance: Conditionally render portals to avoid unnecessary DOM operations * Accessibility: Add proper ARIA attributes for modals and dialogs * Event Bubbling: Remember events bubble through React tree, not DOM tree * Cleanup: Properly cleanup event listeners and portal nodes Portals are powerful tools that help you create professional, accessible UI components that break out of container constraints while maintaining React's component architecture and TypeScript's type safety. # WebSockets vs Server-Sent Events When building real-time applications that require server-to-client communication, two technologies stand out: **WebSockets** and **Server-Sent Events (SSE)**. Both allow servers to push data to clients in real-time, but they have different architectures, capabilities, and ideal use cases. **NOTE:** This post assumes you have a basic understanding of HTTP, JavaScript, and web development concepts. ## What Are WebSockets? [#what-are-websockets] WebSockets provide a full-duplex communication channel over a single TCP connection. Unlike HTTP, which is request-response based, WebSockets allow both the client and server to send messages at any time without needing to request anything. ### Key Characteristics [#key-characteristics] * **Full-duplex**: Both client and server can send messages simultaneously * **Persistent connection**: Once established, the connection stays open * **Low latency**: No overhead of HTTP headers for each message * **Binary data support**: Efficient for sending binary data like images or files ### WebSocket Connection Flow [#websocket-connection-flow] ### WebSocket Example (Client) [#websocket-example-client] ```tsx title="websocket-client-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; function useWebSocket(url: string) { const [isConnected, setIsConnected] = useState(false); const [lastMessage, setLastMessage] = useState(null); const wsRef = useRef(null); const connect = useCallback(() => { if (wsRef.current?.readyState === WebSocket.OPEN) { return; } const ws = new WebSocket(url); ws.onopen = () => { console.log('WebSocket connected'); setIsConnected(true); }; ws.onmessage = (event) => { console.log('Received:', event.data); setLastMessage(event.data); }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; ws.onclose = () => { console.log('WebSocket disconnected'); setIsConnected(false); // Auto-reconnect logic here }; wsRef.current = ws; }, [url]); const send = useCallback((message: string | object) => { if (wsRef.current?.readyState === WebSocket.OPEN) { const data = typeof message === 'string' ? message : JSON.stringify(message); wsRef.current.send(data); } else { console.error('WebSocket is not connected'); } }, []); const disconnect = useCallback(() => { wsRef.current?.close(); }, []); useEffect(() => { connect(); return () => { disconnect(); }; }, [connect, disconnect]); return { isConnected, lastMessage, send, disconnect, connect }; } // Usage in a component function WebSocketComponent() { const { isConnected, lastMessage, send } = useWebSocket('ws://localhost:8080'); return (

Status: {isConnected ? 'Connected' : 'Disconnected'}

Last message: {lastMessage}

); } ```
```tsx title="websocket-client-class.tsx" twoslash class WebSocketClient { private ws: WebSocket | null = null; private url: string; constructor(url: string) { this.url = url; } connect(): void { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('WebSocket connected'); }; this.ws.onmessage = (event) => { console.log('Received:', event.data); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; this.ws.onclose = () => { console.log('WebSocket disconnected'); // Auto-reconnect logic here }; } send(message: string | object): void { if (this.ws?.readyState === WebSocket.OPEN) { const data = typeof message === 'string' ? message : JSON.stringify(message); this.ws.send(data); } else { console.error('WebSocket is not connected'); } } disconnect(): void { this.ws?.close(); } } // Usage const client = new WebSocketClient('ws://localhost:8080'); client.connect(); client.send({ type: 'greeting', message: 'Hello Server!' }); ```
## What Are Server-Sent Events? [#what-are-server-sent-events] Server-Sent Events (SSE) is a server-push technology that enables a client to receive automatic updates from a server via an HTTP connection. Unlike WebSockets, SSE is **unidirectional** - only the server can send messages to the client. ### Key Characteristics [#key-characteristics-1] * **Unidirectional**: Only server can send messages to client * **Uses HTTP**: Built on top of standard HTTP * **Automatic reconnection**: Built-in reconnection handling * **Text-based**: Only supports text data (JSON, plain text) * **Simple API**: Easier to implement than WebSockets ### SSE Connection Flow [#sse-connection-flow] ### SSE Example (Client) [#sse-example-client] ```tsx title="sse-client-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; function useEventSource(url: string) { const [isConnected, setIsConnected] = useState(false); const [lastMessage, setLastMessage] = useState(null); const [notifications, setNotifications] = useState([]); const eventSourceRef = useRef(null); const connect = useCallback(() => { if (eventSourceRef.current) { return; } const eventSource = new EventSource(url); eventSource.onopen = () => { console.log('SSE connected'); setIsConnected(true); }; eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Received:', data); setLastMessage(data); }; eventSource.onerror = (error) => { console.error('SSE error:', error); // Browser automatically attempts reconnection }; // Listen for specific event types eventSource.addEventListener('notification', (event) => { const data = JSON.parse((event as MessageEvent).data); console.log('Notification:', data); setNotifications((prev) => [...prev, data]); }); eventSourceRef.current = eventSource; }, [url]); const disconnect = useCallback(() => { eventSourceRef.current?.close(); eventSourceRef.current = null; setIsConnected(false); }, []); useEffect(() => { connect(); return () => { disconnect(); }; }, [connect, disconnect]); return { isConnected, lastMessage, notifications, disconnect, connect }; } // Usage in a component function SSEComponent() { const { isConnected, lastMessage, notifications } = useEventSource('http://localhost:8080/events'); return (

Status: {isConnected ? 'Connected' : 'Disconnected'}

Last message: {JSON.stringify(lastMessage)}

    {notifications.map((n, i) => (
  • {JSON.stringify(n)}
  • ))}
); } ```
```tsx title="sse-client-class.tsx" twoslash class SSEClient { private eventSource: EventSource | null = null; private url: string; constructor(url: string) { this.url = url; } connect(): void { this.eventSource = new EventSource(this.url); this.eventSource.onopen = () => { console.log('SSE connected'); }; this.eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Received:', data); }; this.eventSource.onerror = (error) => { console.error('SSE error:', error); // Browser automatically attempts reconnection }; // Listen for specific event types this.eventSource.addEventListener('notification', (event) => { const data = JSON.parse(event.data); console.log('Notification:', data); }); } disconnect(): void { this.eventSource?.close(); } } // Usage const client = new SSEClient('http://localhost:8080/events'); client.connect(); ```
## Key Differences Comparison [#key-differences-comparison] | Feature | WebSockets | Server-Sent Events | | --------------------- | -------------------------------------- | ---------------------------------------- | | **Direction** | Full-duplex (bidirectional) | Unidirectional (server → client only) | | **Protocol** | WebSocket protocol (ws\:// or wss\://) | HTTP/HTTPS | | **Reconnection** | Manual implementation required | Built-in automatic reconnection | | **Data Types** | Text and binary data | Text only (JSON, plain text) | | **Browser Support** | Excellent (all modern browsers) | Excellent (all modern browsers) | | **Proxy/Firewall** | May have issues with some proxies | Works seamlessly with HTTP | | **Server Complexity** | Higher (requires stateful connection) | Lower (can use standard HTTP) | | **Scalability** | More complex scaling | Easier to scale with HTTP infrastructure | | **Latency** | Lower overhead | Slightly higher overhead | | **Use Case** | Real-time collaboration, gaming, chat | News feeds, stock prices, notifications | ## When to Use WebSockets [#when-to-use-websockets] ### Real-Time Collaboration [#real-time-collaboration] WebSockets are ideal for applications where multiple users interact simultaneously: ```tsx title="collaboration-example.tsx" // Real-time document editing class DocumentCollaborator { private ws: WebSocket; constructor(docId: string) { this.ws = new WebSocket(`ws://localhost:8080/collab/${docId}`); this.ws.onmessage = (event) => { const update = JSON.parse(event.data); switch (update.type) { case 'text_insert': this.handleTextInsert(update); break; case 'text_delete': this.handleTextDelete(update); break; case 'cursor_move': this.handleCursorMove(update); break; } }; } sendChange(change: object): void { this.ws.send(JSON.stringify(change)); } // Other methods... } ``` ### Chat Applications [#chat-applications] Chat applications require bidirectional communication: ```tsx title="chat-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; interface ChatMessage { id: string; user: string; text: string; timestamp: number; } function useChat(roomId: string) { const [messages, setMessages] = useState([]); const [isConnected, setIsConnected] = useState(false); const wsRef = useRef(null); const connect = useCallback(() => { const ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`); ws.onopen = () => { setIsConnected(true); }; ws.onmessage = (event) => { const message: ChatMessage = JSON.parse(event.data); setMessages((prev) => [...prev, message]); }; ws.onclose = () => { setIsConnected(false); }; wsRef.current = ws; }, [roomId]); const sendMessage = useCallback((text: string) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send( JSON.stringify({ type: 'message', text, timestamp: Date.now() }) ); } }, []); useEffect(() => { connect(); return () => { wsRef.current?.close(); }; }, [connect]); return { messages, isConnected, sendMessage }; } // Usage function ChatRoom() { const { messages, isConnected, sendMessage } = useChat('room123'); return (
Connected: {isConnected ? 'Yes' : 'No'}
    {messages.map((m) => (
  • {m.user}: {m.text}
  • ))}
); } ```
```tsx title="chat-class.tsx" interface ChatMessage { id: string; user: string; text: string; timestamp: number; } class ChatClient { private ws: WebSocket; connect(roomId: string): void { this.ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`); this.ws.onmessage = (event) => { const message: ChatMessage = JSON.parse(event.data); this.displayMessage(message); }; } sendMessage(text: string): void { this.ws.send( JSON.stringify({ type: 'message', text, timestamp: Date.now() }) ); } private displayMessage(message: ChatMessage): void { // Display message logic } } ```
### Online Gaming [#online-gaming] Games require low-latency, bidirectional communication: ```tsx title="gaming-functional.tsx" import React, { useState, useEffect, useCallback, useRef } from 'react'; interface GameState { players: PlayerState[]; ball: BallState; score: Score; } interface PlayerAction { type: string; data: any; } function useGameClient(gameId: string) { const [gameState, setGameState] = useState(null); const [isConnected, setIsConnected] = useState(false); const wsRef = useRef(null); const connect = useCallback(() => { if (wsRef.current?.readyState === WebSocket.OPEN) { return; } const ws = new WebSocket(`ws://localhost:8080/game/${gameId}`); ws.onopen = () => { console.log('Game connected'); setIsConnected(true); }; ws.onmessage = (event) => { const state: GameState = JSON.parse(event.data); setGameState(state); }; ws.onerror = (error) => { console.error('Game WebSocket error:', error); }; ws.onclose = () => { console.log('Game disconnected'); setIsConnected(false); }; wsRef.current = ws; }, [gameId]); const sendAction = useCallback((action: PlayerAction) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify(action)); } else { console.error('Game WebSocket is not connected'); } }, []); const disconnect = useCallback(() => { wsRef.current?.close(); }, []); useEffect(() => { connect(); return () => { disconnect(); }; }, [connect, disconnect]); return { gameState, isConnected, sendAction, disconnect, connect }; } // Usage in a component function GameBoard({ gameId }: { gameId: string }) { const { gameState, isConnected, sendAction } = useGameClient(gameId); const handleMove = (direction: string) => { sendAction({ type: 'move', data: { direction } }); }; return (

Status: {isConnected ? 'Connected' : 'Disconnected'}

{gameState && (

Score: {gameState.score}

)}
); } ```
```tsx title="gaming-class.tsx" interface GameState { players: PlayerState[]; ball: BallState; score: Score; } class GameClient { private ws: WebSocket; connect(gameId: string): void { this.ws = new WebSocket(`ws://localhost:8080/game/${gameId}`); this.ws.onmessage = (event) => { const state: GameState = JSON.parse(event.data); this.render(state); }; } sendAction(action: PlayerAction): void { this.ws.send(JSON.stringify(action)); } } ```
## When to Use Server-Sent Events [#when-to-use-server-sent-events] ### Live Feeds and Updates [#live-feeds-and-updates] SSE is perfect for one-way data streams like live feeds: ```tsx title="live-feed-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; function useLiveFeed(feedType: string) { const [feed, setFeed] = useState([]); const [isConnected, setIsConnected] = useState(false); const eventSourceRef = useRef(null); const subscribe = useCallback(() => { if (eventSourceRef.current) { return; } const eventSource = new EventSource(`/api/feeds/${feedType}`); eventSource.onopen = () => { console.log(`${feedType} feed connected`); setIsConnected(true); }; eventSource.onmessage = (event) => { const update = JSON.parse(event.data); setFeed((prev) => [...prev, update]); }; eventSource.onerror = (error) => { console.error('Feed error:', error); }; eventSourceRef.current = eventSource; }, [feedType]); const unsubscribe = useCallback(() => { eventSourceRef.current?.close(); eventSourceRef.current = null; setIsConnected(false); }, []); useEffect(() => { subscribe(); return () => { unsubscribe(); }; }, [subscribe, unsubscribe]); return { feed, isConnected, unsubscribe, subscribe }; } // Usage for stock prices function StockTicker() { const { feed } = useLiveFeed('stock-prices'); return (

Stock Prices

    {feed.map((item, i) => (
  • {JSON.stringify(item)}
  • ))}
); } // Usage for news updates function NewsFeed() { const { feed } = useLiveFeed('news'); return (

News Updates

    {feed.map((item, i) => (
  • {JSON.stringify(item)}
  • ))}
); } ```
```tsx title="live-feed-class.tsx" class LiveFeedClient { private eventSource: EventSource; subscribe(feedType: string): void { this.eventSource = new EventSource(`/api/feeds/${feedType}`); this.eventSource.onmessage = (event) => { const update = JSON.parse(event.data); this.updateFeed(update); }; } } // Usage for stock prices const stockFeed = new LiveFeedClient(); stockFeed.subscribe('stock-prices'); // Usage for news updates const newsFeed = new LiveFeedClient(); newsFeed.subscribe('news'); ```
### Notification Systems [#notification-systems] SSE excels at delivering notifications: ```tsx title="notification-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; interface Notification { id: string; type: 'info' | 'warning' | 'error' | 'success'; title: string; message: string; timestamp: number; } function useNotifications(userId: string) { const [notifications, setNotifications] = useState([]); const eventSourceRef = useRef(null); const connect = useCallback(() => { if (eventSourceRef.current) { return; } const eventSource = new EventSource(`/api/notifications/${userId}`); eventSource.addEventListener('notification', (event) => { const notification: Notification = JSON.parse((event as MessageEvent).data); console.log(`[${notification.type}] ${notification.title}: ${notification.message}`); setNotifications((prev) => [...prev, notification]); }); eventSource.onerror = (error) => { console.error('SSE error:', error); }; eventSourceRef.current = eventSource; }, [userId]); const disconnect = useCallback(() => { eventSourceRef.current?.close(); eventSourceRef.current = null; }, []); useEffect(() => { connect(); return () => { disconnect(); }; }, [connect, disconnect]); return { notifications, disconnect, connect }; } // Usage in a component function NotificationCenter({ userId }: { userId: string }) { const { notifications } = useNotifications(userId); return (

Notifications

    {notifications.map((n) => (
  • [{n.type}] {n.title}: {n.message}
  • ))}
); } ```
```tsx title="notification-class.tsx" interface Notification { id: string; type: 'info' | 'warning' | 'error' | 'success'; title: string; message: string; timestamp: number; } class NotificationClient { private eventSource: EventSource; connect(userId: string): void { this.eventSource = new EventSource(`/api/notifications/${userId}`); this.eventSource.addEventListener('notification', (event) => { const notification: Notification = JSON.parse(event.data); this.showNotification(notification); }); } private showNotification(notification: Notification): void { // Display notification to user console.log(`[${notification.type}] ${notification.title}: ${notification.message}`); } } ```
### Progress Updates [#progress-updates] SSE is great for long-running operations: ```tsx title="progress-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; interface ProgressUpdate { jobId: string; progress: number; status: string; completed: boolean; } function useJobProgress(jobId: string) { const [progress, setProgress] = useState(null); const [isCompleted, setIsCompleted] = useState(false); const eventSourceRef = useRef(null); const trackJob = useCallback(() => { if (eventSourceRef.current) { return; } const eventSource = new EventSource(`/api/jobs/${jobId}/progress`); eventSource.onmessage = (event) => { const update: ProgressUpdate = JSON.parse(event.data); setProgress(update); if (update.completed) { setIsCompleted(true); eventSource.close(); eventSourceRef.current = null; } }; eventSource.onerror = (error) => { console.error('SSE error:', error); }; eventSourceRef.current = eventSource; }, [jobId]); const stopTracking = useCallback(() => { eventSourceRef.current?.close(); eventSourceRef.current = null; }, []); useEffect(() => { trackJob(); return () => { stopTracking(); }; }, [trackJob, stopTracking]); return { progress, isCompleted, stopTracking }; } // Usage in a component function JobTracker({ jobId }: { jobId: string }) { const { progress, isCompleted } = useJobProgress(jobId); return (

Job Progress

{progress ? (

Status: {progress.status}

Progress: {progress.progress}%

{isCompleted &&

Job completed!

}
) : (

Connecting...

)}
); } ```
```tsx title="progress-class.tsx" twoslash interface ProgressUpdate { jobId: string; progress: number; status: string; completed: boolean; } class ProgressTracker { trackJob(jobId: string): void { const eventSource = new EventSource(`/api/jobs/${jobId}/progress`); eventSource.onmessage = (event) => { const update: ProgressUpdate = JSON.parse(event.data); this.updateProgressBar(update.progress); if (update.completed) { eventSource.close(); this.showCompletionMessage(); } }; } private updateProgressBar(progress: number): void { console.log(`Progress: ${progress}%`); } private showCompletionMessage(): void { console.log('Job completed!'); } } ```
## Real-Time Data Streaming [#real-time-data-streaming] SSE is excellent for streaming real-time data: ```tsx title="streaming-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; function useDataStream(metrics: string[]) { const [streamData, setStreamData] = useState([]); const [isConnected, setIsConnected] = useState(false); const eventSourceRef = useRef(null); const startStream = useCallback(() => { if (eventSourceRef.current) { return; } const params = new URLSearchParams({ metrics: metrics.join(',') }); const eventSource = new EventSource(`/api/stream?${params}`); eventSource.onopen = () => { console.log('Data stream connected'); setIsConnected(true); }; eventSource.onmessage = (event) => { const data = JSON.parse(event.data); setStreamData((prev) => [...prev, data]); }; eventSource.onerror = (error) => { console.error('Stream error:', error); }; eventSourceRef.current = eventSource; }, [metrics]); const stopStream = useCallback(() => { eventSourceRef.current?.close(); eventSourceRef.current = null; setIsConnected(false); }, []); useEffect(() => { startStream(); return () => { stopStream(); }; }, [startStream, stopStream]); return { streamData, isConnected, startStream, stopStream }; } // Usage in a component function DataDashboard({ metrics }: { metrics: string[] }) { const { streamData, isConnected } = useDataStream(metrics); return (

Real-Time Dashboard

Status: {isConnected ? 'Connected' : 'Disconnected'}

    {streamData.map((data, i) => (
  • {JSON.stringify(data)}
  • ))}
); } ```
```tsx title="streaming-class.tsx" twoslash class DataStreamClient { private eventSource: EventSource | null = null; startStream(metrics: string[]): void { const params = new URLSearchParams({ metrics: metrics.join(',') }); this.eventSource = new EventSource(`/api/stream?${params}`); this.eventSource.onmessage = (event) => { const data = JSON.parse(event.data); this.updateDashboard(data); }; } stopStream(): void { this.eventSource?.close(); } private updateDashboard(data: any): void { // Update UI with streamed data } } ```
## Combining Both Technologies [#combining-both-technologies] Sometimes the best approach is to use both technologies together based on the use case: ```tsx title="hybrid-functional.tsx" twoslash import React, { useState, useEffect, useCallback, useRef } from 'react'; function useHybridClient(roomId: string, userId: string) { const [collabData, setCollabData] = useState(null); const [notifications, setNotifications] = useState([]); const wsRef = useRef(null); const eventSourceRef = useRef(null); // Use WebSocket for real-time collaboration const connectCollaboration = useCallback(() => { if (wsRef.current?.readyState === WebSocket.OPEN) { return; } const ws = new WebSocket(`ws://localhost:8080/collab/${roomId}`); ws.onopen = () => { console.log('Collaboration WebSocket connected'); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); setCollabData(data); }; ws.onerror = (error) => { console.error('Collaboration WebSocket error:', error); }; wsRef.current = ws; }, [roomId]); // Use SSE for notifications const subscribeNotifications = useCallback(() => { if (eventSourceRef.current) { return; } const eventSource = new EventSource(`/api/notifications/${userId}`); eventSource.addEventListener('notification', (event) => { const notification = JSON.parse((event as MessageEvent).data); setNotifications((prev) => [...prev, notification]); }); eventSourceRef.current = eventSource; }, [userId]); const disconnectAll = useCallback(() => { wsRef.current?.close(); eventSourceRef.current?.close(); eventSourceRef.current = null; }, []); useEffect(() => { connectCollaboration(); subscribeNotifications(); return () => { disconnectAll(); }; }, [connectCollaboration, subscribeNotifications, disconnectAll]); return { collabData, notifications, disconnectAll }; } // Usage in a component function HybridApp({ roomId, userId }: { roomId: string; userId: string }) { const { collabData, notifications } = useHybridClient(roomId, userId); return (

Collaborative Room

{collabData &&
Collab Data: {JSON.stringify(collabData)}
}

Notifications

    {notifications.map((n, i) => (
  • {JSON.stringify(n)}
  • ))}
); } ```
```tsx title="hybrid-class.tsx" twoslash class HybridClient { private ws: WebSocket | null = null; private eventSource: EventSource | null = null; // Use WebSocket for real-time collaboration connectCollaboration(roomId: string): void { this.ws = new WebSocket(`ws://localhost:8080/collab/${roomId}`); this.ws.onmessage = (event) => this.handleCollaboration(event); } // Use SSE for notifications subscribeNotifications(userId: string): void { this.eventSource = new EventSource(`/api/notifications/${userId}`); this.eventSource.addEventListener('notification', (event) => { this.handleNotification(event); }); } private handleCollaboration(event: MessageEvent): void { // Handle collaboration updates } private handleNotification(event: MessageEvent): void { // Handle notifications } } ```
## Server-Side Examples [#server-side-examples] ### Node.js WebSocket Server [#nodejs-websocket-server] ```ts title="websocket-server.ts" import { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', (ws) => { console.log('Client connected'); ws.on('message', (data) => { // Broadcast to all clients wss.clients.forEach((client) => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(data); } }); }); ws.on('close', () => { console.log('Client disconnected'); }); }); ``` ### Node.js SSE Server [#nodejs-sse-server] ```ts title="sse-server.ts" import express from 'express'; const app = express(); app.get('/events', (req, res) => { // Set headers for SSE res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Send initial message res.write('event: connected\ndata: {"message": "Connected!"}\n\n'); // Send updates every second const interval = setInterval(() => { const data = JSON.stringify({ timestamp: Date.now(), value: Math.random() }); res.write(`data: ${data}\n\n`); }, 1000); // Clean up on client disconnect req.on('close', () => { clearInterval(interval); }); }); app.listen(8080); ``` ## Decision Framework [#decision-framework] Use this decision tree to choose between WebSockets and SSE: ``` Do you need client → server communication? ├── Yes → Use WebSockets └── No → Continue Do you need binary data support? ├── Yes → Use WebSockets └── No → Continue Is simplicity more important than lowest latency? ├── Yes → Use SSE └── No → Consider WebSockets ``` ## Conclusion [#conclusion] Both WebSockets and Server-Sent Events are powerful technologies for real-time communication: * Choose **WebSockets** for interactive, bidirectional applications like chat, collaboration, and gaming * Choose **SSE** for one-way data streaming like notifications, live feeds, and updates Consider your specific requirements: communication direction, data types, complexity, and scalability needs. Often, the right choice becomes clear when you evaluate what you're trying to achieve rather than choosing based on technical features alone.