Skip to content
  • LC
    Luis Castro

    Jun 15, 2026

    Future-proof components

    useMemo is a performance hint, not a guarantee. React is free to throw the cached value away whenever it wants, and increasingly it does, during hot reloads, for offscreen content, or under future optimizations. That is fine when you use it for speed. It is a bug when you use it for correctness.

    The fragile assumption

    Each user gets a stable avatar gradient derived from their id. If the gradient is regenerated, the color visibly changes, which looks broken.

    function Avatar({ user }) {
      const gradient = useMemo(
        () => generateGradient(user.id),
        [user.id],
      )
    
      return <div style={gradient}>{user.initials}</div>
    }

    generateGradient may include some randomness or expensive hashing. If React drops the memo and recomputes, the user’s color shifts under them. You were relying on useMemo to persist a value, which it never promised to do.

    The fix

    When correctness depends on a value sticking around, store it in state. State is a guarantee. Recompute it explicitly only when the input that matters actually changes.

    function Avatar({ user }) {
      const [gradient, setGradient] = useState(() => generateGradient(user.id))
      const [prevId, setPrevId] = useState(user.id)
    
      if (user.id !== prevId) {
        setPrevId(user.id)
        setGradient(generateGradient(user.id))
      }
    
      return <div style={gradient}>{user.initials}</div>
    }

    The gradient now persists for the lifetime of the user and changes only when the id does. Use useMemo for speed, use state for promises. None of these are edge cases anymore. They are the conditions your components already ship into, so it is worth building for them from the start.

  • LC
    Luis Castro

    Jun 14, 2026

    Leak-proof components

    In a Server Components world, the boundary between server and client is a single prop away. Pass the wrong object across it and you serialize secrets straight into the HTML. On a social app the obvious danger is the session object, which often carries an auth token right next to the harmless display name.

    The accidental leak

    async function ProfileHeader() {
      const session = await getSession()
    
      // FollowButton is a Client Component
      return <FollowButton session={session} />
    }

    The whole session object, including session.token, gets serialized and shipped to the browser so a client component can read one field. The token is now sitting in the page payload for anyone to grab.

    The fix

    React’s experimental taint APIs mark a value as server only. If anything tries to send it to the client, React throws instead of silently serializing it.

    import { experimental_taintUniqueValue } from 'react'
    
    async function ProfileHeader() {
      const session = await getSession()
    
      experimental_taintUniqueValue(
        'Do not pass the session token to the client.',
        session,
        session.token,
      )
    
      // Pass only what the client actually needs
      return <FollowButton viewerId={session.userId} />
    }

    Tainting turns a quiet data leak into a loud build time error. The safest secret is the one the framework refuses to let you ship.

  • LC
    Luis Castro

    Jun 13, 2026

    Activity-proof components

    React’s <Activity> component lets you keep a tab rendered but hidden, so switching back to it is instant. On a social app you might keep the Messages tab warm while the user browses the feed. The trap is global side effects. A hidden component that injects styles into the page keeps applying them even while nobody can see it.

    The lingering styles

    Say the Messages tab enables a focus theme that dims the rest of the app.

    function FocusTheme({ children }) {
      return (
        <>
          <style>{`
            :root {
              --feed-dim: 0.4;
            }
          `}</style>
          {children}
        </>
      )
    }

    When <Activity> hides this tab, the component is not unmounted, so its <style> tag stays in the document. The feed stays dimmed even though Messages is no longer on screen.

    The fix

    useLayoutEffect cleanup runs when the activity is hidden and reruns when it is shown. Toggle the media attribute on the style tag to switch it off without unmounting.

    function FocusTheme({ children }) {
      const ref = useRef(null)
    
      useLayoutEffect(() => {
        const style = ref.current
        if (!style) return
        style.media = 'all'
        return () => {
          style.media = 'not all'
        }
      }, [])
    
      return (
        <>
          <style ref={ref}>{`
            :root {
              --feed-dim: 0.4;
            }
          `}</style>
          {children}
        </>
      )
    }

    Now the styles apply only while the tab is visible and stand down the instant it is hidden.

  • LC
    Luis Castro

    Jun 12, 2026

    Transition-proof components

    React 19 ships View Transitions, which let DOM swaps animate smoothly instead of snapping. Switching between the For You and Following feeds is the perfect candidate. The catch is that a normal state update paints instantly, so you get no animation at all unless you tell React the update is allowed to take its time.

    The instant swap

    function FeedTabs() {
      const [tab, setTab] = useState('forYou')
    
      return (
        <>
          {tab === 'forYou' ? <ForYouFeed /> : <FollowingFeed />}
          <button onClick={() => setTab('following')}>Following</button>
          <button onClick={() => setTab('forYou')}>For You</button>
        </>
      )
    }

    The feeds replace each other in a single frame. No cross fade, no slide, just a hard cut.

    The fix

    Wrap the update in startTransition. React then treats the swap as a transition and the View Transitions API can animate between the two states.

    import { startTransition, useState } from 'react'
    
    function FeedTabs() {
      const [tab, setTab] = useState('forYou')
    
      const switchTo = (next) =>
        startTransition(() => setTab(next))
    
      return (
        <>
          {tab === 'forYou' ? <ForYouFeed /> : <FollowingFeed />}
          <button onClick={() => switchTo('following')}>Following</button>
          <button onClick={() => switchTo('forYou')}>For You</button>
        </>
      )
    }

    Same logic, but now the feeds animate as they change. The component is ready for transitions instead of fighting them.

  • LC
    Luis Castro

    Jun 11, 2026

    Portal-proof components

    Social apps love pop-out windows. A direct message thread you tear off into its own window, a composer that opens in a separate browser window so people can keep scrolling. The moment your component renders into a portal or a pop-out, the global window stops being the window it actually lives in.

    The wrong window

    Here is a keyboard shortcut that opens the composer when the user presses the letter n.

    function ComposerShortcut({ onOpen }) {
      useEffect(() => {
        const handler = (e) => {
          if (e.key === 'n') onOpen()
        }
        window.addEventListener('keydown', handler)
        return () => window.removeEventListener('keydown', handler)
      }, [onOpen])
    
      return null
    }

    If this component renders inside a pop-out chat window, it still listens on the main window. Pressing n in the pop-out does nothing, because the listener is attached to the wrong document entirely.

    The fix

    Reach for the document the component is actually mounted in, through ownerDocument.defaultView. Fall back to window for the normal case.

    function ComposerShortcut({ onOpen }) {
      const ref = useRef(null)
    
      useEffect(() => {
        const win = ref.current?.ownerDocument.defaultView || window
        const handler = (e) => {
          if (e.key === 'n') onOpen()
        }
        win.addEventListener('keydown', handler)
        return () => win.removeEventListener('keydown', handler)
      }, [onOpen])
    
      return <span ref={ref} hidden />
    }

    Now the shortcut binds to whichever window the component truly lives in, portal or not.

  • LC
    Luis Castro

    Jun 10, 2026

    Composition-proof components

    A common pattern is a wrapper that injects data into its children. The old way to do it was cloneElement, passing extra props down into whatever you were handed. In a world of Server Components and lazy boundaries, that approach quietly falls apart.

    The brittle version

    Imagine a provider that wants every child to know who the current viewer is.

    function CurrentUserProvider({ user, children }) {
      return Children.map(children, (child) =>
        cloneElement(child, { viewer: user }),
      )
    }

    This breaks the moment a child is a Server Component, a lazy component, or a promise. Those children are opaque references. They are not plain elements you can clone and spread props onto, so React either throws or silently drops the prop.

    The fix

    Use Context. It passes data through the tree without ever touching the shape of the children, so it does not care what kind of element each child is.

    const ViewerContext = createContext(null)
    
    function CurrentUserProvider({ user, children }) {
      return (
        <ViewerContext.Provider value={user}>
          {children}
        </ViewerContext.Provider>
      )
    }
    
    function FollowButton({ targetId }) {
      const viewer = use(ViewerContext)
      const isSelf = viewer?.id === targetId
    
      return <button disabled={isSelf}>Follow</button>
    }

    Children stay opaque, the data still flows, and the wrapper composes with anything you hand it.

  • LC
    Luis Castro

    Jun 9, 2026

    Concurrent-proof components

    Server Components let you fetch data right where you render it, which is wonderful until you realize how often the same component shows up. An author avatar renders on the post, on every comment, on the hover card, and in the notifications rail. If each one fetches the author independently, a single feed can fire hundreds of identical database queries.

    The duplication

    Every instance does its own round trip.

    async function AuthorAvatar({ userId }) {
      const user = await db.users.get(userId)
    
      return <img src={user.avatarUrl} alt={user.name} />
    }

    Render twelve posts from the same person and you pay for twelve identical lookups in one request.

    The fix

    Wrap the query in React’s cache. Within a single request, repeated calls with the same arguments return the same in flight promise, so the work happens once and every caller shares the result.

    import { cache } from 'react'
    
    const getUser = cache((userId) => db.users.get(userId))
    
    async function AuthorAvatar({ userId }) {
      const user = await getUser(userId)
    
      return <img src={user.avatarUrl} alt={user.name} />
    }

    The component reads as if it fetches its own data, which keeps it composable, but the deduplication happens for free. Concurrency stops being a performance trap.

  • LC
    Luis Castro

    Jun 8, 2026

    Instance-proof components

    The inline script trick from last time has a hidden assumption baked in. It hardcodes getElementById('feed'). That works right up until two of these components appear on the same screen, which on a social network happens constantly. A profile preview inside a feed, a quoted post inside another post, a comment thread inside a modal.

    The collision

    With a fixed id, the second instance overwrites the first. Both scripts target the same DOM node, so whichever runs last wins and the other instance silently breaks.

    function FeedDensityProvider({ children }) {
      return (
        <>
          <div id="feed">{children}</div>
          <script
            dangerouslySetInnerHTML={{
              __html: `
                const density = localStorage.getItem('feedDensity') || 'cozy'
                document.getElementById('feed').dataset.density = density
              `,
            }}
          />
        </>
      )
    }

    The fix

    React gives you useId for exactly this. It produces a stable, unique id per instance that matches between server and client, so each copy of the component talks only to its own node.

    function FeedDensityProvider({ children }) {
      const id = useId()
    
      return (
        <>
          <div id={id}>{children}</div>
          <script
            dangerouslySetInnerHTML={{
              __html: `
                try {
                  const density = localStorage.getItem('feedDensity') || 'cozy'
                  document.getElementById('${id}').dataset.density = density
                } catch (e) {}
              `,
            }}
          />
        </>
      )
    }

    Never hardcode an identifier in a component meant to be reused. The moment it ships, someone will render two of them next to each other.

  • LC
    Luis Castro

    Jun 7, 2026

    Hydration-proof components

    Deferring browser reads to useEffect keeps the server happy, but it introduces a new annoyance. The user sees the default first, then a flash as the real value snaps in. On a feed, that means the layout jumps from comfortable density to compact the instant React hydrates.

    The flash

    The effect based version is correct, but it always paints the wrong value for one frame.

    function FeedDensityProvider({ children }) {
      const [density, setDensity] = useState('cozy')
    
      useEffect(() => {
        setDensity(localStorage.getItem('feedDensity') || 'cozy')
      }, [])
    
      return <div data-density={density}>{children}</div>
    }

    Anyone who set their feed to compact gets a visible jump on every page load.

    The fix

    Inject a tiny synchronous script that sets the correct value before the browser paints and before React hydrates. By the time React takes over, the DOM already holds the right state, so there is nothing to correct.

    function FeedDensityProvider({ children }) {
      return (
        <>
          <div id="feed">{children}</div>
          <script
            dangerouslySetInnerHTML={{
              __html: `
                try {
                  const density = localStorage.getItem('feedDensity') || 'cozy'
                  document.getElementById('feed').dataset.density = density
                } catch (e) {}
              `,
            }}
          />
        </>
      )
    }

    The script runs inline, synchronously, before paint. No flash, no jump, no flicker on the feed.

  • LC
    Luis Castro

    Jun 5, 2026

    Server-proof components

    A component that works on your laptop is not the same as a component that works for everyone. The real test is not whether it renders on the page you are building right now. It is whether it survives when someone drops it into a server-rendered route, a portal, or a feed that paints a thousand copies of it.

    The problem

    Say you store the viewer’s preferred feed sort, top posts or latest posts, in localStorage. The naive version reads it during render.

    function FeedPreferencesProvider({ children }) {
      const sort = localStorage.getItem('feedSort') || 'top'
    
      return <div data-feed-sort={sort}>{children}</div>
    }

    This crashes the moment the feed route is server rendered. There is no localStorage on the server, so the render throws before a single post reaches the browser.

    The fix

    Browser only APIs should be touched only in the browser. Defer the read to useEffect, which never runs on the server.

    function FeedPreferencesProvider({ children }) {
      const [sort, setSort] = useState('top')
    
      useEffect(() => {
        setSort(localStorage.getItem('feedSort') || 'top')
      }, [])
    
      return <div data-feed-sort={sort}>{children}</div>
    }

    Now the server renders the safe default and the client upgrades to the saved preference once it mounts. The component stops caring where it runs, which is the whole point.

  • LC
    Luis Castro

    May 10, 2026

    Most apps built with agents are complex and hard to change. Because agents can radically speed up coding, they also accelerate software entropy. Codebases get more complex at an unprecedented rate. Common questions: Why do I have to explain best practices to if it supposedly was built and trained on all the material about coding best practices? My agents should automatically have all the knowledge of my app, why are they not using X from my codebase? The fix for this is a radical new approach to AI-powered development: caring about how you think and design the code.

  • LC
    Luis Castro

    May 1, 2026

    AI continues shaping things out in the Software world, if you are in the ecosystem or read the X threads about the advancement on the models, it feels like the implementation is rapidly becoming a solved problem, right? Writing code is now fast, it’s getting cheap, and quality is going up and to the right. The hard question is no longer how to build it. It’s should we build it. Agreeing on what to build is the new bottleneck.

  • LC
    Luis Castro

    Apr 30, 2026

    Here we are. End of April, we just had the perfect date of the year, not too hot, not too cold, all we needed was a light jacket.

    A few things I loved

    Fashion

    Not a light jacket, the MA-1 Bomber jacket was a jacket given to me by Digg, and I really liked the look of it. I did have to do a few modifications before I wore it, nothing too difficult.

    We are starting to see the hotter months of spring, and summer will be here soon, so it is time for the mighty t-shirt to come out. The staple of any closet for the next ~6 months.

    I got two recommendations here: the Lady-White Co collection and Whitesville Quality T. Both will seem expensive, but after years of wearing these and washing them with almost no care for the cycle I used on my washer, well, these are alive and well. Something I cannot say about my Uniqlo U ones, although the Supima Cotton Uniqlo U price is… Well, you have choices here.

    Travel

    I went to Chicago for work. Most days it was rainy, and it didn’t matter much because I was at an office. But I played pickleball and went to a Blackhawks vs. Sharks hockey game and I had a blast. I guess my recommendation here is: if you are in the US, take the time to go to a hockey game. It is, weirdly enough, the easiest sport to enjoy if you are not into sports.

    Fun fact: all the hockey games I’ve watched have been in different cities, but the San Jose Sharks were always one of the teams playing. Pure coincidence, though.

    Books

    Beacon 23 by Hugh Howey, found the book on the science fiction aisle of a great bookstore while I was in Chicago that a friend took me to. I started reading it on my flight back home. I haven’t finished yet but I’m hooked. I also learned that there’s a TV show with mixed reviews on Rotten Tomatoes. I might give it a shot once I finish the book.

    Anime

    A friend recommended Frieren: Beyond Journey’s End and honestly, what a surprise it’s been. The most common criticism is that it’s boring, but I’d argue that’s entirely missing the point. The slow pace is intentional. The show deprioritizes plot and conflict in favor of character and theme, telling rich and emotionally complete stories even within a single half-episode.

    The rare high-stakes moments hit harder because of the quiet buildup around them, and each episode is structured so satisfyingly that it stands alone without context. In a genre dominated by action-heavy battles, this show does almost the opposite: minimal conflict, unhurried storytelling, and emotional payoffs rooted in realistic characters and relationships. The catch is that this formula is all-or-nothing. If it doesn’t click with you, the slowness has nothing to carry it. For me, it clicked immediately.

    Computer nerd corner

    AI everything. I have been playing with local agents on my server and idle computers, running a few open models and just giving them free rein in a few Docker containers to do silly things. I have been playing more with setting up flows than actually making products with them, but it is part of the fun.

  • LC
    Luis Castro

    Mar 26, 2026

    Still testing the game screen designs. I know they don’t need to be perfect before I launch to TestFlight users, but I want them in a state I’m proud of. Day 6 of #BuildingInPublic. I’m going to lay low this weekend, take the dog to the mountains, and step away from the screen. Starting somewhere new on Monday 🎉

  • LC
    Luis Castro

    Mar 25, 2026

    Light post today. I had lunch with friends and we discussed many fun topics: software development, crypto, and the state of AI in our day to day. I also met someone through a friend who lives in the same town, and I’m pretty sure we’ll grab coffee soon.

  • LC
    Luis Castro

    Mar 24, 2026

    The design is coming along. I now have the board set and a color theme for at least the light mode. I had a great talk with a friend yesterday, we discussed the game and he offered to be an early test user. He also suggested a campaign mode, which I’m now figuring out how to seed without using too much local storage. Day 5 of #BuildingInPublic.

  • LC
    Luis Castro

    Mar 23, 2026

    Thinking about the game’s design. I’m aiming for a vibe similar to paper app Dungeon notebooks. Now I need to translate that vision into an actual design. I’m not a designer, but I collaborate with them daily, so I should manage something and refine it with a few designer friends. Day 4 of #BuildingInPublic.

  • LC
    Luis Castro

    Mar 21, 2026

    Taking it slow today since it’s a nice spring day. The cherries are in full bloom, and I went to my favorite coffee shop in town. The owners had their two lovely dogs with them, but mine isn’t a big fan of them.

  • LC
    Luis Castro

    Mar 20, 2026

    Today was interesting. I focused on core mechanics. The base is Sudoku, but with twists to help or slow you down. All ideas were handwritten, so tomorrow I’ll implement them to see what sticks. Day 3 of #BuildingInPublic. In my mind it’s feeling good 🧐

  • LC
    Luis Castro

    Mar 19, 2026

    Day 2 of #BuildingInPublic. I already had a board rules engine with solid test coverage and good adaptability, built for simple games (less complex than chess). I had a lot of fun making it but never used it, so today I’m forking and adapting it for this game.

    Also, I do wonder, how much should I be sharing? My goal is to be held accountable and ship it, instead of leaving it like my board rules engine, gathering dust in one of the corners of private GitHub 😅. If you have comments and thoughts, please send me an email!

  • LC
    Luis Castro

    Mar 18, 2026

    I’ve been wanting to build something purely for fun. No client, no deadline, just a project where I get to experiment with game mechanics, play with React Native and Expo, and learn by doing. Sudoku felt like the perfect canvas for that.

    Does the world need another Sudoku game? Probably not. But I’ve been playing Sudoku on and off for years and at this point my brain is so wired to the numbers that it almost feels like autopilot. I want to shake that up a bit, give it a twist that makes you think differently. That’s the experiment.

    Before getting fancy though, I want to nail the fundamentals. The plan is to start with the classic Sudoku experience done well: four difficulty levels, infinite generated boards, and a clean, satisfying feel. Once that’s solid, I’ll start layering in the twists.

    The part I’m most excited about is the social side. I want to add a daily puzzle, similar to what Wordle did, where everyone plays the same board and can share their results. There’s something great about that shared experience, comparing times with friends, the friendly competition of it all.

    On the tech side, I’m going all in on Expo and React Native. Part of the fun is seeing how far I can push them for a game-like experience. It’s a great excuse to dig into animations, gestures, and the kind of polished interactions you don’t always get to explore in typical app work.

    More updates as the project takes shape. I’m trying to #BuildInPublic which should also be a refreshing experience. In theory…

  • LC
    Luis Castro

    Mar 17, 2026

    At home, I had about 6 smart outlets, 8 smart switches and 2 sensors pre-installed by the builder. I wasn’t able to use any of them because they “required” a proprietary app tied to a cloud service only available on the Spanish App Store, which I’m not on. A shame, because I really wanted to control the blinds from my phone. The switches are as far from the bed as they could possibly be, making it a hassle every night and morning.

    So I said enough is enough and invested the time to understand how these worked. Glad to find they were Zigbee-controlled, which gave me the flexibility to finally set up Home Assistant for the first time.

    I bought another Raspberry Pi 4 board and followed the installation guide. Big props to the team for making it so easy to set up, it almost felt like plug and play. I made a few hardware choices that are not typical. I went with a SanDisk High Endurance SD Card instead of a performance-oriented one since I’m not running any heavy workloads, just a simple home automation setup. I also went with a PoE hat for power because I have a Unifi network already supplying power to basically all the ethernet ports in my home. The most important part, the antenna: I didn’t buy the one Home Assistant advertises. I went with a SONOFF Zigbee 3.0 & Thread Dongle Lite (EFR32MG21), which I was a bit hesitant about given its size, but it has been rock solid.

    So have I done it? Achieved a better life of automation? It feels like early days for the house. Doing this has opened me up to experiment and tinker with what I’d like to set up next. It is a dangerous hobby though. You mess with it too much and risk bricking your setup and annoying everyone at home who now relies on it. So I’m going to slowly plan things out.

  • LC
    Luis Castro

    Mar 17, 2026

    Still thinking about how mesmerizing art can be… I have probably revisited this photo on my phone at least 10 times after my trip to Edinburgh.

  • LC
    Luis Castro

    Mar 16, 2026

    The revamp of the website is now live! Honestly the newest version of Astro has made it so fun to build, the transitions API is incredibly smooth, this past year I focused a lot on React Native, Swift and Kotlin, and comming back to the web even for a few days is such a refreshing view.

  • LC
    Luis Castro

    Mar 16, 2026

    After shipping several React Native apps with complex list UIs, I settled on a set of patterns around FlashList that consistently perform well. Read more to uncover my thought process, strategies like wrapping FlashList in a reusable component, tuning props like drawDistance and recycle pool size differently for iOS and Android, and gating on screen focus to avoid visual artifacts with react-navigation.

    Let’s also take a dive into react-native-gesture-handler’s ScrollView. Why it is an upgrade on iOS, but on Android it has some really bad behaviors that will significantly affect your User Experience.