react-postprocessing icon indicating copy to clipboard operation
react-postprocessing copied to clipboard

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

Open ajie123100 opened this issue 11 months ago • 14 comments

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

ajie123100 avatar Mar 02 '25 08:03 ajie123100

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.

givensuman avatar Mar 27 '25 19:03 givensuman

This also happens to me

kengru avatar Apr 01 '25 02:04 kengru

Same here, on latest version of all packages the Select API doesn't seem to work due to this error

thirstyfish avatar Apr 03 '25 23:04 thirstyfish

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

Arunakemi avatar Apr 10 '25 18:04 Arunakemi

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?

Arunakemi avatar Apr 12 '25 05:04 Arunakemi

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"

stefvw93 avatar Apr 26 '25 12:04 stefvw93

I'm also experiencing this problem.

samuelgoodell avatar May 01 '25 17:05 samuelgoodell

me too.. please help..

Suprhimp avatar May 03 '25 16:05 Suprhimp

same here, it's a real shame :(

alexandre-jurisoft avatar Jul 08 '25 23:07 alexandre-jurisoft

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"
  }

Bossieh avatar Jul 10 '25 07:07 Bossieh

From my understanding, this is what causes the infinite loop:

  1. Object Tracking and State Comparison (Infinite Loop Cause 1):
  • The useEffect hook iterates through all Object3D instances within the group to determine if a state change is needed (changed flag is set if api.selected.indexOf(o) === -1).
  • However, only Mesh objects are subsequently added to the api.selected state 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.select is called on every render, resulting in an uncontrolled infinite render loop.
  1. useEffect Cleanup 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 the api.selected state when the component unmounts.
  • However, because the useEffect hook 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

dragospaulpop avatar Jul 31 '25 16:07 dragospaulpop

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>
    );
}

devloop01 avatar Aug 11 '25 17:08 devloop01

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>
  )
}

dklwo avatar Sep 18 '25 21:09 dklwo

PR: https://github.com/pmndrs/react-postprocessing/pull/342

dklwo avatar Sep 18 '25 21:09 dklwo