hook.js:608 Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. Error Component Stack
import React from 'react' import { EffectComposer, Outline, Selection, Select, } from '@react-three/postprocessing' import { Canvas } from '@react-three/fiber' import { OrbitControls } from '@react-three/drei' const App = () => { return ( <Canvas gl={{ antialias: true }} // 启用抗锯齿 onCreated={({ gl }) => { gl.setClearColor('rgb(50, 50, 50)') }} > <OrbitControls enablePan={true} // 允许平移 enableZoom={true} // 允许缩放 enableRotate={true} // 允许旋转 minDistance={5} // 最小缩放距离 maxDistance={20} // 最大缩放距离 minPolarAngle={Math.PI / 4} maxPolarAngle={Math.PI / 2} /> <ambientLight intensity={2} /> <directionalLight color='#fff' intensity={2} position={[0, 0, 5]} />
<Selection enabled>
<EffectComposer autoClear={false}>
<Outline blur edgeStrength={10} />
</EffectComposer>
<Select enabled> // Why is there a problem with enabled here?
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color='#000' />
</mesh>
</Select>
</Selection>
</Canvas>
) }
export default App
Second, this error spams the console in my attempt at implementing it. Was curious so I made a local copy of the "Selective outlines" example project and it happens there too. It's not very noticeable in the example but it also causes severe performance issues in my app where there are more complex models.
This also happens to me
Same here, on latest version of all packages the Select API doesn't seem to work due to this error
Ah sheesh, been fighting with this problem for a whole day. I should have looked around the issues first. Yep it seems Select API is kinda bronken right now. I wanted to use an Outline Effect on hover, but having enabled={hoverVariable} is re-rendering my mesh everytime (resseting position and rotation etc) because its making an update loop
Ah sheesh, been fighting with this problem for a whole day. I should have looked around the issues first. Yep it seems Select API is kinda bronken right now. I wanted to use an Outline Effect on hover, but having enabled={hoverVariable} is re-rendering my mesh everytime (resseting position and rotation etc) because its making an update loop
Ah I just updated my Nodejs version and upgraded all my node modules at it fixed my issue. Maybe try updating everything?
Any update on this? Having the exact same issue; using <Select enabled>. Followed the example almost exactly.
<Selection>
<EffectComposer autoClear={false}>
<Outline blur edgeStrength={100} />
</EffectComposer>
<Select enabled>
{/* some meshes here... */}
</Select>
</Selection>
"@react-three/drei": "^10.0.7",
"@react-three/fiber": "^9.1.2",
"@react-three/postprocessing": "^3.0.4",
"three": "^0.172.0"
I'm also experiencing this problem.
me too.. please help..
same here, it's a real shame :(
Having the same issue
"dependencies": {
"@react-three/drei": "10.4.2",
"@react-three/fiber": "9.2.0",
"@react-three/postprocessing": "3.0.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-error-boundary": "6.0.0",
"three": "0.178.0"
}
From my understanding, this is what causes the infinite loop:
- Object Tracking and State Comparison (Infinite Loop Cause 1):
- The
useEffecthook iterates through all Object3D instances within the group to determine if a state change is needed (changed flag is set ifapi.selected.indexOf(o) === -1). - However, only Mesh objects are subsequently added to the
api.selectedstate via the current array. - This discrepancy means that non-mesh objects within the group are checked against
api.selected(which exclusively contains meshes). For any non-mesh object,api.selected.indexOf(o)will always be -1, causing the changed flag to be perpetually true if any non-mesh object exists in the group. - Consequently,
api.selectis called on every render, resulting in an uncontrolled infinite render loop.
-
useEffectCleanup Always Alters State (Infinite Loop Cause 2):
- The cleanup function
api.select((state) => state.filter((selected) => !current.includes(selected)))correctly attempts to remove the currently added meshes from theapi.selectedstate when the component unmounts. - However, because the
useEffecthook is re-run after each addition or removal, the cleanup function is guaranteed to be called, it then alters the state resulting in an infinite loop.
This might not be optimal as I'm new to the ecosystem, but here's my implementation of the hook with proper functionality:
useEffect(() => {
// run triggered through initialization or state update
if (api) {
const toBeAdded: THREE.Object3D[] = [];
const toBeRemoved: THREE.Object3D[] = [];
const current: THREE.Object3D[] = [];
group.current.traverse((o) => {
if (o.type === "Mesh") {
// keep a track of all meshes in the group, to be referenced in the cleanup function
current.push(o);
// check if the mesh is already selected
const alreadySelected = api.selected.includes(o);
// if the mesh is not selected and the selection is enabled, mark it for selection
// if the mesh is selected and the selection is disabled, mark it for removal
if (enabled && !alreadySelected) {
toBeAdded.push(o);
} else if (!enabled && alreadySelected) {
toBeRemoved.push(o);
}
}
});
// add the meshes that are not selected and the selection is enabled
// this will trigger a re-run of the useEffect hook
if (toBeAdded.length > 0) {
api.select((state) => {
return [...state, ...toBeAdded];
});
}
// remove the meshes that are selected and the selection is disabled
// this will trigger a re-run of the useEffect hook
if (toBeRemoved.length > 0) {
api.select((state) => {
return state.filter((o) => !toBeRemoved.includes(o));
});
}
// if there's nothing to add or remove the useEffect hook will not be re-run and everything stops here
// cleanup function runs before the body of the next useEffect hook re-run
return () => {
// the cleanup function only handles objects removed from the scene
// if a mesh doesn't have a parent, it means it's not attached to a scene and we can remove it from the selection
// so that we don't hog the memory with deleted objects
const orphaned = current.filter((o) => o.parent === null);
if (orphaned.length > 0) {
api.select((state) => {
return state.filter((o) => !orphaned.includes(o));
});
}
return;
};
}
}, [enabled, children, api]);
Logic:
A. for a selection action:
- enabled changes to true which causes the useEffect hook to run
- objects are added to the state, triggering a re-run of the useEffect hook
- cleanup function runs, does nothing because there's no orphaned objects
- useEffect hook re-runs and sees that there's nothing to add or remove
- useEffect hook stops here
B. for a un-selection action:
- enabled changes to false which causes the useEffect hook to run
- objects are removed from the state, triggering a re-run of the useEffect hook
- cleanup function runs, does nothing because there's no orphaned objects
- useEffect hook re-runs and sees that there's nothing to add or remove
- useEffect hook stops here
C. for an unmount action:
- useEffect cleanup runs, all objects are orphaned, so we remove them from the state
- useEffect hook is destroyed
I fixed this issue by updating the dependency array inside the Select Component.
export function Select({ enabled = false, children, ...props }: SelectApi) {
const group = useRef<THREE.Group>(null!);
const api = useContext(selectionContext);
useEffect(() => {
// ...
}, [enabled]); // <--- remove children, api
return (
<group ref={group} {...props}>
{children}
</group>
);
}
So the issue is react 19 surfaces an existing issue where Select is reactively consuming an api that it also pushes updates to
Proposed Fix: create seperate api for select component and effect components to consume
You can also copy and use these modified components in the mean time
import {Api} from "@react-three/postprocessing/src/Selection";
import React, {createContext, useContext, useEffect, useMemo, useRef, useState, Provider} from "react";
import {selectionContext, SelectApi} from "@react-three/postprocessing"
import * as THREE from "three";
export const selectContext = /* @__PURE__ */ createContext<Api | null>(null)
export function ModifiedSelect({ enabled = false, children, ...props }: SelectApi) {
const group = useRef<THREE.Group>(null!)
const api = useContext(selectContext)
useEffect(() => {
if (api && enabled) {
let changed = false
const current: THREE.Object3D[] = []
group.current.traverse((o) => {
if (o.type === 'Mesh') {
current.push(o)
}
if (api.selected.indexOf(o) === -1) changed = true
})
if (changed) {
api.select((state) => [...state, ...current])
return () => {
api.select((state) => state.filter((selected) => !current.includes(selected)))
}
}
}
}, [enabled, children, api]);
return (
<group ref={group} {...props}>
{children}
</group>
)
}
export function ModifiedSelection(
{ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }
) {
const [selected, select] = useState<THREE.Object3D[]>([])
const selectApiRef = useRef({ selected, select, enabled })
const selectApi = selectApiRef.current
selectApi.selected = selected
selectApi.select = select
selectApi.enabled = enabled
const selectionApi = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
return (
<selectContext.Provider value={selectApiRef.current}>
<selectionContext.Provider value={selectionApi}>
{children}
</selectionContext.Provider>
</selectContext.Provider>
)
}
PR: https://github.com/pmndrs/react-postprocessing/pull/342