fix(lint): satisfy react-hooks immutability + set-state-in-effect rules
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
The new react-compiler-aware lint rules flag legitimate Three.js external-system synchronizations (texture/uniform/AnimationAction mutations) and a derived-state reset in PylonDownedPylon. None of these are bugs — they're the canonical way to bridge React state with imperative graphics objects — so they're annotated with targeted eslint-disable comments and a small reorder. - EbikeGPSMap: disable on uniform/texture sync effects - EbikeSpeedmeter: disable around the canvas+texture useFrame sync - PylonFarmerNPC: disable around playAnim (drei AnimationAction fadeIn/fadeOut/setLoop/clampWhenFinished) and the effects/frame callbacks that invoke it - PylonDownedPylon: move showUpright/isPylonInteractive declarations above the useFrame that reads them (fixes access-before-declared) and disable set-state-in-effect on the per-step isRaised reset
This commit is contained in:
@@ -181,6 +181,8 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
|
||||
// Sync texture into uniform when it changes (canvas resize)
|
||||
useEffect(() => {
|
||||
// External Three.js material uniform sync — intentional side effect.
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
shaderMat.uniforms.map.value = texture;
|
||||
}, [shaderMat, texture]);
|
||||
|
||||
@@ -196,6 +198,8 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
// Resize the canvas whenever canvasSize changes (texture declared above)
|
||||
useEffect(() => {
|
||||
Object.assign(offscreenCanvas, { width: canvasSize, height: canvasSize });
|
||||
// External Three.js texture invalidation — intentional side effect.
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
texture.needsUpdate = true;
|
||||
}, [canvasSize, offscreenCanvas, texture]);
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ export function EbikeSpeedmeter({
|
||||
);
|
||||
|
||||
// ── Frame loop ──────────────────────────────────────────────────────────────
|
||||
/* External Three.js canvas+texture sync — intentional side effects in useFrame. */
|
||||
/* eslint-disable react-hooks/immutability */
|
||||
useFrame((_, delta) => {
|
||||
// 1. Smooth speed factor
|
||||
const target = THREE.MathUtils.clamp(window.ebikeSpeedFactor ?? 0, 0, 1);
|
||||
@@ -181,6 +183,7 @@ export function EbikeSpeedmeter({
|
||||
}
|
||||
|
||||
fillTexture.needsUpdate = true;
|
||||
/* eslint-enable react-hooks/immutability */
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,9 +30,26 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
const straightenStartRef = useRef<number | null>(null);
|
||||
const hasPlayedFirstAudioRef = useRef(false);
|
||||
|
||||
const showUpright =
|
||||
isRaised ||
|
||||
mainState !== "pylon" ||
|
||||
step === "waiting" ||
|
||||
step === "inspected" ||
|
||||
step === "fragmented" ||
|
||||
step === "scanning" ||
|
||||
step === "repairing" ||
|
||||
step === "reassembling" ||
|
||||
step === "done" ||
|
||||
step === "narrator-outro";
|
||||
|
||||
const isPylonInteractive = step === "arrived" || step === "npc-return";
|
||||
|
||||
useEffect(() => {
|
||||
if (step === "arrived") {
|
||||
hasPlayedFirstAudioRef.current = false;
|
||||
// Reset the "raised" latch when a new run begins. This is derived
|
||||
// resync from the step prop and runs once per step transition.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsRaised(false);
|
||||
}
|
||||
}, [step]);
|
||||
@@ -62,20 +79,6 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
);
|
||||
});
|
||||
|
||||
const showUpright =
|
||||
isRaised ||
|
||||
mainState !== "pylon" ||
|
||||
step === "waiting" ||
|
||||
step === "inspected" ||
|
||||
step === "fragmented" ||
|
||||
step === "scanning" ||
|
||||
step === "repairing" ||
|
||||
step === "reassembling" ||
|
||||
step === "done" ||
|
||||
step === "narrator-outro";
|
||||
|
||||
const isPylonInteractive = step === "arrived" || step === "npc-return";
|
||||
|
||||
const beginStraighten = (): void => {
|
||||
setIsStraightening(true);
|
||||
pylonStraighteningSignal.started = true;
|
||||
|
||||
@@ -34,7 +34,10 @@ const _target = new THREE.Vector3();
|
||||
* Compute the Y rotation (radians) for a model whose default forward
|
||||
* direction is +Z, so that it faces from `from` toward `to`.
|
||||
*/
|
||||
function faceToward(from: THREE.Vector3, to: readonly [number, number, number]): number {
|
||||
function faceToward(
|
||||
from: THREE.Vector3,
|
||||
to: readonly [number, number, number],
|
||||
): number {
|
||||
const dx = to[0] - from.x;
|
||||
const dz = to[2] - from.z;
|
||||
return Math.atan2(dx, dz);
|
||||
@@ -71,6 +74,10 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
||||
// ─── playAnim ─────────────────────────────────────────────────────────────
|
||||
// NOTE: actions is intentionally in the dep array so this callback is
|
||||
// recreated when drei's internal state populates the actions map.
|
||||
// External THREE.AnimationAction lifecycle methods (fadeOut/fadeIn/play +
|
||||
// setLoop/clampWhenFinished mutations) are intentional side effects on
|
||||
// drei-managed objects.
|
||||
/* eslint-disable react-hooks/immutability */
|
||||
const playAnim = useCallback(
|
||||
(name: NPCAnimation, fade = ANIM_FADE): void => {
|
||||
if (currentAnimRef.current === name) return;
|
||||
@@ -89,6 +96,7 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
||||
},
|
||||
[actions],
|
||||
);
|
||||
/* eslint-enable react-hooks/immutability */
|
||||
|
||||
// ─── Async audio after pylon is raised ────────────────────────────────────
|
||||
const playPostRaiseAudioAndAdvance = useCallback(async () => {
|
||||
@@ -112,6 +120,8 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
||||
|
||||
// ─── Step-driven animation ────────────────────────────────────────────────
|
||||
// Fires when step changes OR when playAnim changes (i.e. when actions load).
|
||||
// playAnim mutates drei-managed AnimationAction internals (intentional).
|
||||
/* eslint-disable react-hooks/immutability */
|
||||
useEffect(() => {
|
||||
currentAnimRef.current = null;
|
||||
if (step === "arrived") {
|
||||
@@ -168,7 +178,10 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
||||
currentPosRef.current.lerp(_target, t);
|
||||
} else if (!isStraightening && currentAnimRef.current === "walk") {
|
||||
playAnim("idle");
|
||||
savedRotationYRef.current = faceToward(currentPosRef.current, PYLON_WORLD_POSITION);
|
||||
savedRotationYRef.current = faceToward(
|
||||
currentPosRef.current,
|
||||
PYLON_WORLD_POSITION,
|
||||
);
|
||||
}
|
||||
group.position.copy(currentPosRef.current);
|
||||
} else if (step === "inspected") {
|
||||
@@ -180,8 +193,15 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
||||
}
|
||||
|
||||
// ── Rotation ──────────────────────────────────────────────────────────
|
||||
if (step === "npc-return" && !isCompleted && currentAnimRef.current === "walk") {
|
||||
const walkRotY = faceToward(currentPosRef.current, PYLON_FARMER_NPC_WALK_LOOK_AT);
|
||||
if (
|
||||
step === "npc-return" &&
|
||||
!isCompleted &&
|
||||
currentAnimRef.current === "walk"
|
||||
) {
|
||||
const walkRotY = faceToward(
|
||||
currentPosRef.current,
|
||||
PYLON_FARMER_NPC_WALK_LOOK_AT,
|
||||
);
|
||||
group.rotation.set(0, walkRotY, 0);
|
||||
} else {
|
||||
group.rotation.set(0, savedRotationYRef.current, 0);
|
||||
@@ -189,6 +209,7 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
||||
|
||||
group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE);
|
||||
});
|
||||
/* eslint-enable react-hooks/immutability */
|
||||
|
||||
if (mainState !== "pylon") return null;
|
||||
if (step !== "arrived" && step !== "npc-return" && step !== "inspected")
|
||||
|
||||
Reference in New Issue
Block a user