fix: Resolve state-during-render anti-pattern in usePatternTransform

Moved state synchronization logic from render phase to useEffect hooks to prevent potential infinite loops and unpredictable behavior. Implemented ref-based previous value tracking to detect genuine parent prop changes without causing cascading renders.

Changes:
- Replaced direct setState calls during render with properly structured useEffect hooks
- Added prevOffsetRef and prevRotationRef to track previous prop values
- Documented the "partially controlled" pattern needed for Konva drag interactions
- Added justified ESLint disable comments for legitimate setState-in-effect usage

This fixes a critical React anti-pattern that could cause performance issues and render loops.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-12-26 21:33:32 +01:00
parent 7fd31d209c
commit 47e32ef83d

View file

@ -39,22 +39,41 @@ export function usePatternTransform({
const patternGroupRef = useRef<Konva.Group | null>(null); const patternGroupRef = useRef<Konva.Group | null>(null);
const transformerRef = useRef<Konva.Transformer | null>(null); const transformerRef = useRef<Konva.Transformer | null>(null);
// Update pattern offset when initialPatternOffset changes // Track previous prop values to detect external changes
if ( const prevOffsetRef = useRef(initialPatternOffset);
initialPatternOffset && const prevRotationRef = useRef(initialPatternRotation);
(localPatternOffset.x !== initialPatternOffset.x ||
localPatternOffset.y !== initialPatternOffset.y)
) {
setLocalPatternOffset(initialPatternOffset);
}
// Update pattern rotation when initialPatternRotation changes // Sync local state with parent props when they change externally
if ( // This implements a "partially controlled" pattern needed for Konva drag interactions:
initialPatternRotation !== undefined && // - Local state enables optimistic updates during drag/transform (immediate visual feedback)
localPatternRotation !== initialPatternRotation // - Parent props sync when external changes occur (e.g., pattern upload resets position)
) { // - Previous value refs prevent sync loops by only updating when props genuinely change
setLocalPatternRotation(initialPatternRotation); useEffect(() => {
} if (
initialPatternOffset &&
(prevOffsetRef.current.x !== initialPatternOffset.x ||
prevOffsetRef.current.y !== initialPatternOffset.y)
) {
// This setState in effect is intentional and safe: it only runs when the parent
// prop changes, not in response to our own local updates
// eslint-disable-next-line react-hooks/set-state-in-effect
setLocalPatternOffset(initialPatternOffset);
prevOffsetRef.current = initialPatternOffset;
}
}, [initialPatternOffset]);
useEffect(() => {
if (
initialPatternRotation !== undefined &&
prevRotationRef.current !== initialPatternRotation
) {
// This setState in effect is intentional and safe: it only runs when the parent
// prop changes, not in response to our own local updates
// eslint-disable-next-line react-hooks/set-state-in-effect
setLocalPatternRotation(initialPatternRotation);
prevRotationRef.current = initialPatternRotation;
}
}, [initialPatternRotation]);
// Attach/detach transformer based on state // Attach/detach transformer based on state
const attachTransformer = useCallback(() => { const attachTransformer = useCallback(() => {