Insights
Today I Learned
Short, practical notes from the Haliviq team: quick lessons from daily engineering and design work. Each one is something we ran into while building, with the reason it happens and what we do about it now. Filter by topic to find notes on React, Next.js, PostgreSQL, Docker, Figma, security and more.
Wrap heavy list re-renders with useDeferredValue instead of debouncing
Debounce only delays the update, so the expensive render still blocks the page once it fires. useDeferredValue lets React interrupt that render when a new keystroke arrives, so typing stays smooth even on a 5,000-row table. Reach for it when a filter box drives a big list and you do not want to add arbitrary wait times.
Read notekey={index} silently breaks Framer Motion exit animations
When you reorder or remove items in a list keyed by index, React matches the wrong old node to the new item, so the exit animation plays on the wrong element. Use a stable id from your data as the key. It costs nothing and also fixes odd input-focus and state bugs in the same list.
Read noteuseEffect cleanup running twice in Strict Mode isn't a bug to fix
In development, React 18 mounts, unmounts and re-mounts your component on purpose to surface effects that are not idempotent. If running it twice breaks something, such as a duplicate subscription or a double request, the effect was already unsafe. Fix the cleanup rather than turning Strict Mode off.
Read noteServer Components re-render on every navigation, not just mount
We assumed the output of a Server Component was cached across route changes. In practice Next.js fetches it again on every soft navigation unless you opt into a caching strategy. If a page feels slow after a client-side link click, check what the server component is fetching and decide explicitly what should be cached and for how long.
Read noteMulti-stage builds cut our image size by 71%
Copying node_modules straight from the build stage dragged in compilers and dev tools that nobody needs at runtime. A clean multi-stage build with COPY --from, copying only the built output and production dependencies, cut the image from 1.2GB to 340MB. Smaller images pull faster and have fewer packages to patch.
Read noteColor contrast checkers lie about gradient text
Most WCAG tools sample a single color, so a gradient heading can pass the check while the lighter end is hard to read for real people. Test the lightest and darkest points of the gradient against the background, or avoid gradients on small and important text altogether.
Read notePartial indexes made our soft-delete queries 40x faster
We were indexing 2M soft-deleted rows that the app never queries. A partial index with WHERE deleted_at IS NULL covers only live rows, so it is smaller and fits in memory, and it cut query time from 800ms to 20ms. Any table where most queries hit a small, well-defined slice of rows is a good candidate.
Read noteDebouncing search input isn't enough — you need request cancellation too
Even with debounce, a slow response can arrive after a newer one and overwrite it with stale results. Creating an AbortController per request and aborting the previous one fixed the old results flashing in for fast typists. Cancelling also saves the server from finishing work nobody will see.
Read noteQuick Look caches a stale thumbnail after a file gets overwritten
Run qlmanage -r to reset the cache. Without it, a re-exported PNG can keep showing yesterday's preview in Finder for hours, which is confusing when you are checking design exports or handing off assets.
Read noteRotating a leaked API key is not the same as revoking its sessions
Signed sessions that were issued with the old key stayed valid for 24 hours after we rotated it, so an attacker could still use them. Our incident checklist now always revokes active sessions and tokens as well as replacing the key, and then checks the logs for use of the old one.
Read noterevalidatePath doesn't bust the client-side router cache
The server data was fresh, but users still saw stale content for up to 30 seconds because the browser keeps its own router cache that revalidatePath does not touch. When freshness matters, such as after a form submit, trigger a router refresh on the client as well, and test the flow by clicking around rather than only reloading.
Read notesatisfies keeps literal types AND checks the shape
Annotating a variable with a type widens it, so you lose the exact literal values. The satisfies operator validates the object against the type but keeps the narrow inference, which is handy for config objects and route maps where you want both autocomplete on the real keys and a safety net on the shape.
Read noteEXPLAIN ANALYZE lies about buffer cache on the first run
The first execution pulls cold pages from disk, so the timing looks worse than what users will normally see. Always run it twice and read the second pass, and add BUFFERS to see how much came from cache. Comparing a cold run with a warm one is a common way to misjudge whether an index helped.
Read noteThai line-height needs ~1.7, not the 1.5 that works for Latin text
Thai script stacks vowels and tone marks above and below the baseline, so a Latin-friendly line-height of 1.5 clips them or makes them touch the next line. Bumping body text to about 1.7 fixed clipped vowels across the whole site. Test with words that carry stacked marks, and check headings separately since they usually need less.
Read noteOptional chaining hides typos in property names from the compiler
If the type is loose or any, user?.emial quietly returns undefined instead of raising an error, and the typo ships to production unnoticed. Keep the object properly typed so the compiler can flag the misspelling, and treat a lot of optional chaining as a hint that the data model is not well defined.
Read note.dockerignore doesn't apply to COPY --from another stage
We kept finding .env in the final image because .dockerignore only filters the build context sent to Docker, not files copied between stages. Be explicit about what you copy in later stages, never copy the whole directory, and inspect the final image to confirm no secrets are inside.
Read noteAuto layout 'hug contents' breaks silently on empty strings
A frame set to hug its contents collapses to zero width when the bound text is empty, and the layers panel gives no warning. Give components a minimum width, or design an explicit empty state, so a missing value in real data does not make an element disappear.
Read notefzf + ripgrep beats grep -r for anything in a monorepo
rg --files | fzf gives fuzzy file jumping, and wrapping rg in fzf with a preview window gives live content search. In a big monorepo this brought our search time down to almost nothing, mostly because ripgrep respects .gitignore and skips build folders by default.
Read noteHave a project in mind?
We'd love to hear what you're building.
These notes come from our daily design and engineering work in Bangkok. If you are planning a website, app or AI tool and want a team that pays attention to details like these, tell us about it.