Skip to content

Learnings

Key insights discovered during the frontend implementation of Item Image Upload.

  • What worked: Mutation hooks (useMutation) for upload, reachability check, and external image fetch compose cleanly. Each hook exposes mutateAsync, isPending, error, and reset — the bridge hook (useItemImageUploader) simply composes three mutations into one ImageUploader interface.
  • Surprise: mutateAsync references from TanStack are stable across renders, so manual useMemo/useCallback wrapping is unnecessary and conflicts with React Compiler’s preserve-manual-memoization rule. Let the compiler handle memoization.

Design-system typed provider pattern (FD-01)

Section titled “Design-system typed provider pattern (FD-01)”
  • What worked: The ImageUploadProvider + useImageUploader context pattern eliminated 3 callback props per consumer component and removed parallel-implementation drift between ItemCardEditor and ImageUploadDialog.
  • 5.0.0 migration was smooth: Consumer changes were mechanical — replace 3 props with a <Provider value={uploader}> wrapper. Test mocks needed updating but followed a predictable pattern.
  • Lesson: When a component needs more than 2 injectable behaviors that share a common concern, context is the right abstraction from the start. The 3-callback approach accumulated technical debt across 4.11.x versions.
  • Key discovery: AG Grid’s cell-editor lifecycle is fundamentally incompatible with React portal-based modal editors. cellEditorPopup: true wraps the editor in a 0x0 container (hidden subtree defeats Radix focus traps). Without it, stopEditingWhenCellsLoseFocus tears down the editor when the portal steals focus.
  • Pattern established: Grid becomes the trigger (double-click / Enter / F2), the wrapper component owns the editor lifecycle, and commit flows back through applyTransaction + dirty-row tracking. This pattern is generalizable to any portalled modal editor in AG Grid.

Canvas export and react-easy-crop coordinate spaces

Section titled “Canvas export and react-easy-crop coordinate spaces”
  • Hard lesson: react-easy-crop’s croppedAreaPixels is always clamped to the image’s natural pixel bounds, even at zoom < 1. At zoom = 0.5, it returns {x:0, y:0, width:W, height:H} (the full image), not a rect extending beyond bounds. The zoom scalar is a separate value that must be applied in the canvas export.
  • Three failed fix attempts before correctly diagnosing the coordinate space: (1) clamping negative coords that were never negative, (2) removing zoom entirely, (3) two-path draw that still took the wrong path because onCropComplete fires with non-zero width at zoom < 1.
  • Takeaway: Unit tests with 1x1 mock images cannot validate coordinate geometry. The only reliable validation for canvas export is visual verification at realistic dimensions, or a Storybook play function that asserts pixel colors on the output canvas.
  • Problem: Amplify preview deployments and localhost are not on *.arda.cards — CloudFront signed cookies (domain-scoped) don’t work.
  • Solution: CdnAuthProvider transparently detects the origin and switches between signed cookies (production) and signed URLs (preview / localhost). Components use useResolvedImageUrl and never know which mode is active.
  • Surprise: The ResolvedImageCellDisplay wrapper was needed for AG Grid because cell renderers can’t call hooks directly — the wrapper provides the hook context.
  • What worked: src/server/ for server-only code, src/api/ for SPA API functions, src/app/api/ for thin Next.js route re-exports. Clean separation of concerns.
  • All routes need export const runtime = 'nodejs' — without it, Next.js may attempt Edge deployment, causing runtime errors on dns, crypto, and AWS SDK imports. This was caught by Copilot review.
  • SSRF protection must check DNS resolution — hostname validation alone is insufficient; a public hostname can resolve to a private IP.
  • CHANGELOG conflicts are inevitable in multi-branch workflows. The resolution pattern is: keep both version entries, ensure descending semver order, verify CLQ accepts the result.
  • Squash-merge + rebase = lost entries: When a branch is squash-merged to main and another branch rebases onto main, the squash’d CHANGELOG entry can be silently dropped if both branches modified the same region. Always verify CHANGELOG completeness after rebase.

Copyright: (c) Arda Systems 2025-2026, All rights reserved