diff --git a/packages/ecs/src/QueryFunctions.test.tsx b/packages/ecs/src/QueryFunctions.test.tsx new file mode 100644 index 0000000000..26cf5609a2 --- /dev/null +++ b/packages/ecs/src/QueryFunctions.test.tsx @@ -0,0 +1,116 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { renderHook } from '@testing-library/react' +import assert from 'assert' +import { ComponentMap, defineComponent, hasComponent, removeComponent, setComponent } from './ComponentFunctions' +import { destroyEngine, startEngine } from './Engine' +import { createEntity } from './EntityFunctions' +import { defineQuery, useQuery } from './QueryFunctions' + +const ComponentA = defineComponent({ name: 'ComponentA' }) +const ComponentB = defineComponent({ name: 'ComponentB' }) + +describe('QueryFunctions', () => { + beforeEach(() => { + startEngine() + }) + + afterEach(() => { + ComponentMap.clear() + return destroyEngine() + }) + + describe('defineQuery', () => { + it('should define a query with the given components', () => { + const query = defineQuery([ComponentA, ComponentB]) + assert.ok(query) + let entities = query() + assert.ok(entities) + assert.strictEqual(entities.length, 0) // No entities yet + + const e1 = createEntity() + const e2 = createEntity() + setComponent(e1, ComponentA) + setComponent(e1, ComponentB) + setComponent(e2, ComponentA) + setComponent(e2, ComponentB) + setComponent(createEntity(), ComponentA) + setComponent(createEntity(), ComponentB) + + entities = query() + assert.strictEqual(entities.length, 2) + assert.strictEqual(entities[0], e1) + assert.strictEqual(entities[1], e2) + assert.ok(hasComponent(entities[0], ComponentA)) + assert.ok(hasComponent(entities[0], ComponentB)) + }) + }) + + describe('useQuery', () => { + it('should return entities that match the query', () => { + const e1 = createEntity() + const e2 = createEntity() + setComponent(e1, ComponentA) + setComponent(e1, ComponentB) + setComponent(e2, ComponentA) + setComponent(e2, ComponentB) + const { result } = renderHook(() => useQuery([ComponentA, ComponentB])) // return correct results the first time + const entities = result.current + assert.strictEqual(entities.length, 2) + assert.strictEqual(entities[0], e1) + assert.strictEqual(entities[1], e2) + assert.ok(hasComponent(entities[0], ComponentA)) + assert.ok(hasComponent(entities[0], ComponentB)) + assert.ok(hasComponent(entities[1], ComponentA)) + assert.ok(hasComponent(entities[1], ComponentB)) + }) + + it('should update the entities when components change', () => { + const e1 = createEntity() + const e2 = createEntity() + setComponent(e1, ComponentA) + setComponent(e1, ComponentB) + setComponent(e2, ComponentA) + setComponent(e2, ComponentB) + const { result, rerender } = renderHook(() => useQuery([ComponentA, ComponentB])) + let entities = result.current + assert.strictEqual(entities.length, 2) + assert.strictEqual(entities[0], e1) + assert.strictEqual(entities[1], e2) + assert.ok(hasComponent(entities[0], ComponentA)) + assert.ok(hasComponent(entities[0], ComponentB)) + assert.ok(hasComponent(entities[1], ComponentA)) + assert.ok(hasComponent(entities[1], ComponentB)) + removeComponent(e1, ComponentB) + rerender() + entities = result.current + assert.strictEqual(entities.length, 1) + assert.strictEqual(entities[0], e2) + assert.ok(hasComponent(entities[0], ComponentA)) + assert.ok(hasComponent(entities[0], ComponentB)) + }) + }) +}) diff --git a/packages/ecs/src/QueryFunctions.tsx b/packages/ecs/src/QueryFunctions.tsx index 2a90d26b52..72913d377e 100644 --- a/packages/ecs/src/QueryFunctions.tsx +++ b/packages/ecs/src/QueryFunctions.tsx @@ -24,10 +24,10 @@ Ethereal Engine. All Rights Reserved. */ import * as bitECS from 'bitecs' -import React, { ErrorInfo, FC, memo, Suspense, useEffect, useLayoutEffect, useMemo } from 'react' +import React, { ErrorInfo, FC, memo, Suspense, useLayoutEffect, useMemo } from 'react' import { useForceUpdate } from '@etherealengine/common/src/utils/useForceUpdate' -import { getState, HyperFlux, startReactor, useHookstate } from '@etherealengine/hyperflux' +import { getState, HyperFlux, startReactor, useImmediateEffect } from '@etherealengine/hyperflux' import { Component, useOptionalComponent } from './ComponentFunctions' import { Entity } from './Entity' @@ -69,12 +69,10 @@ export const ReactiveQuerySystem = defineSystem({ uuid: 'ee.hyperflux.ReactiveQuerySystem', insert: { after: PresentationSystemGroup }, execute: () => { - for (const { query, result } of getState(SystemState).reactiveQueryStates) { + for (const { query, forceUpdate } of getState(SystemState).reactiveQueryStates) { const entitiesAdded = query.enter().length const entitiesRemoved = query.exit().length - if (entitiesAdded || entitiesRemoved) { - result.set(query()) - } + if (entitiesAdded || entitiesRemoved) forceUpdate() } } }) @@ -84,17 +82,19 @@ export const ReactiveQuerySystem = defineSystem({ * - "components" argument must not change */ export function useQuery(components: QueryComponents) { - const result = useHookstate([] as Entity[]) + const query = defineQuery(components) + const eids = query() + removeQuery(query) + const forceUpdate = useForceUpdate() - // Use an immediate (layout) effect to ensure that `queryResult` + // Use a layout effect to ensure that `queryResult` // is deleted from the `reactiveQueryStates` map immediately when the current // component is unmounted, before any other code attempts to set it // (component state can't be modified after a component is unmounted) useLayoutEffect(() => { const query = defineQuery(components) - result.set(query()) - const queryState = { query, result, components } + const queryState = { query, forceUpdate, components } getState(SystemState).reactiveQueryStates.add(queryState) return () => { removeQuery(query) @@ -103,21 +103,47 @@ export function useQuery(components: QueryComponents) { }, []) // create an effect that forces an update when any components in the query change - useEffect(() => { - const entities = [...result.value] - const root = startReactor(function useQueryReactor() { - for (const entity of entities) { - components.forEach((C) => ('isComponent' in C ? useOptionalComponent(entity, C as any)?.value : undefined)) - } + // use an immediate effect to ensure that the reactor is initialized even if this component becomes suspended during this render + useImmediateEffect(() => { + function UseQueryEntityReactor({ entity }: { entity: Entity }) { + return ( + <> + {components.map((C) => { + const Component = ('isComponent' in C ? C : (C as any)()[0]) as Component + return ( + + ) + })} + + ) + } + + function UseQueryComponentReactor(props: { entity: Entity; Component: Component }) { + useOptionalComponent(props.entity, props.Component) forceUpdate() return null + } + + const root = startReactor(function UseQueryReactor() { + return ( + <> + {eids.map((entity) => ( + + ))} + + ) }) + return () => { root.stop() } - }, [result]) + }, [JSON.stringify(eids)]) - return result.value + return eids } export type Query = ReturnType diff --git a/packages/ecs/src/SystemFunctions.ts b/packages/ecs/src/SystemFunctions.ts index 356e56a85e..6af3755097 100755 --- a/packages/ecs/src/SystemFunctions.ts +++ b/packages/ecs/src/SystemFunctions.ts @@ -25,12 +25,12 @@ Ethereal Engine. All Rights Reserved. /** Functions to provide system level functionalities. */ -import { FC, useEffect } from 'react' +import { FC } from 'react' import { v4 as uuidv4 } from 'uuid' import { OpaqueType } from '@etherealengine/common/src/interfaces/OpaqueType' import multiLogger from '@etherealengine/common/src/logger' -import { getMutableState, getState, startReactor } from '@etherealengine/hyperflux' +import { getMutableState, getState, startReactor, useImmediateEffect } from '@etherealengine/hyperflux' import { SystemState } from './SystemState' import { nowMilliseconds } from './Timer' @@ -217,7 +217,7 @@ export function defineSystem(systemConfig: SystemArgs) { } export const useExecute = (execute: () => void, insert: InsertSystem) => { - useEffect(() => { + useImmediateEffect(() => { const handle = defineSystem({ uuid: uuidv4(), execute, insert }) return () => { destroySystem(handle) diff --git a/packages/ecs/src/SystemState.ts b/packages/ecs/src/SystemState.ts index c73e21d992..16103200f4 100644 --- a/packages/ecs/src/SystemState.ts +++ b/packages/ecs/src/SystemState.ts @@ -24,9 +24,8 @@ Ethereal Engine. All Rights Reserved. */ import { isDev } from '@etherealengine/common/src/config' -import { defineState, ReactorRoot, State } from '@etherealengine/hyperflux' +import { defineState, ReactorRoot } from '@etherealengine/hyperflux' -import { Entity } from './Entity' import { Query, QueryComponents } from './QueryFunctions' import { SystemUUID } from './SystemFunctions' @@ -36,6 +35,6 @@ export const SystemState = defineState({ performanceProfilingEnabled: isDev, activeSystemReactors: new Map(), currentSystemUUID: '__null__' as SystemUUID, - reactiveQueryStates: new Set<{ query: Query; result: State; components: QueryComponents }>() + reactiveQueryStates: new Set<{ query: Query; forceUpdate: () => void; components: QueryComponents }>() }) }) diff --git a/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx b/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx index aa05a0ceff..39a8310d42 100644 --- a/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx +++ b/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx @@ -245,4 +245,16 @@ describe('ResourceLoaderHooks', () => { }) }) }) + + it('useGLTF calls loadResource synchronously', () => { + const resourceState = getState(ResourceState) + const entity = createEntity() + // use renderHook to render the hook + renderHook(() => { + // call the useGLTF hook + useGLTF(gltfURL, entity) + }) + // ensure that the loadResource function is synchronously called when the hook is rendered + assert(resourceState.resources[gltfURL]) + }) }) diff --git a/packages/engine/src/assets/functions/resourceLoaderHooks.ts b/packages/engine/src/assets/functions/resourceLoaderHooks.ts index b98b8add97..402bdd2e15 100644 --- a/packages/engine/src/assets/functions/resourceLoaderHooks.ts +++ b/packages/engine/src/assets/functions/resourceLoaderHooks.ts @@ -24,12 +24,12 @@ Ethereal Engine. All Rights Reserved. */ import { GLTF } from '@gltf-transform/core' -import { useEffect, useLayoutEffect } from 'react' +import { useEffect } from 'react' import { Texture } from 'three' import { v4 as uuidv4 } from 'uuid' import { Entity, UndefinedEntity } from '@etherealengine/ecs' -import { NO_PROXY, State, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, State, useHookstate, useImmediateEffect } from '@etherealengine/hyperflux' import { ResourceAssetType, ResourceManager, ResourceType } from '@etherealengine/spatial/src/resources/ResourceState' import { ResourcePendingComponent } from '../../gltf/ResourcePendingComponent' @@ -59,7 +59,7 @@ function useLoader( return unload }, []) - useLayoutEffect(() => { + useImmediateEffect(() => { const controller = new AbortController() if (url !== urlState.value) { if (urlState.value) { @@ -145,7 +145,7 @@ function useBatchLoader( return unload }, []) - useEffect(() => { + useImmediateEffect(() => { const completedArr = new Array(urls.length).fill(false) as boolean[] const controller = new AbortController() diff --git a/packages/hyperflux/functions/useImmediateEffect.test.ts b/packages/hyperflux/functions/useImmediateEffect.test.ts new file mode 100644 index 0000000000..c60da58fbf --- /dev/null +++ b/packages/hyperflux/functions/useImmediateEffect.test.ts @@ -0,0 +1,79 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { renderHook } from '@testing-library/react' +import assert from 'assert' +import { useImmediateEffect } from './useImmediateEffect' + +describe('useImmediateEffect', () => { + it('should call the effect function immediately', () => { + let effectCalled = false + const effect = () => { + effectCalled = true + } + + const { rerender } = renderHook((deps: number[]) => useImmediateEffect(effect, deps), { + initialProps: [] + }) + + rerender([]) + + assert(effectCalled) + }) + + it('should call the cleanup function when dependencies change', () => { + let cleanupCalled = false + const effect = () => { + return () => { + cleanupCalled = true + } + } + + const { rerender } = renderHook((deps: number[]) => useImmediateEffect(effect, deps), { + initialProps: [] + }) + + rerender([1, 2, 3]) + + assert(cleanupCalled) + }) + + it('should not call the cleanup function when dependencies do not change', () => { + let cleanupCalled = false + const effect = () => { + return () => { + cleanupCalled = true + } + } + + const { rerender } = renderHook((deps: number[]) => useImmediateEffect(effect, deps), { + initialProps: [1, 2, 3] + }) + + rerender([1, 2, 3]) + + assert(!cleanupCalled) + }) +}) diff --git a/packages/hyperflux/functions/useImmediateEffect.ts b/packages/hyperflux/functions/useImmediateEffect.ts new file mode 100644 index 0000000000..f28e546942 --- /dev/null +++ b/packages/hyperflux/functions/useImmediateEffect.ts @@ -0,0 +1,58 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { DependencyList, EffectCallback, useLayoutEffect, useRef } from 'react' + +function depsDiff(deps1, deps2) { + return !( + Array.isArray(deps1) && + Array.isArray(deps2) && + deps1.length === deps2.length && + deps1.every((dep, idx) => Object.is(dep, deps2[idx])) + ) +} + +export function useImmediateEffect(effect: EffectCallback, deps?: DependencyList) { + const cleanupRef = useRef() + const depsRef = useRef() + + if (!depsRef.current || depsDiff(depsRef.current, deps)) { + depsRef.current = deps + + if (cleanupRef.current) { + cleanupRef.current() + } + + cleanupRef.current = effect() + } + + useLayoutEffect(() => { + return () => { + if (cleanupRef.current) { + cleanupRef.current() + } + } + }, []) +} diff --git a/packages/hyperflux/index.ts b/packages/hyperflux/index.ts index 7094eaee1d..27bf3d3dff 100644 --- a/packages/hyperflux/index.ts +++ b/packages/hyperflux/index.ts @@ -27,3 +27,4 @@ export * from './functions/ActionFunctions' export * from './functions/ReactorFunctions' export * from './functions/StateFunctions' export * from './functions/StoreFunctions' +export * from './functions/useImmediateEffect' diff --git a/packages/spatial/src/input/components/InputComponent.ts b/packages/spatial/src/input/components/InputComponent.ts index 0ee497ffda..5dc57caa7e 100644 --- a/packages/spatial/src/input/components/InputComponent.ts +++ b/packages/spatial/src/input/components/InputComponent.ts @@ -25,14 +25,7 @@ Ethereal Engine. All Rights Reserved. import { useLayoutEffect } from 'react' -import { - getComponent, - getMutableComponent, - getOptionalComponent, - InputSystemGroup, - UndefinedEntity, - useExecute -} from '@etherealengine/ecs' +import { getComponent, getOptionalComponent, InputSystemGroup, UndefinedEntity, useExecute } from '@etherealengine/ecs' import { defineComponent, removeComponent, @@ -70,8 +63,7 @@ export const InputComponent = defineComponent({ //internal /** populated automatically by ClientInputSystem */ - inputSources: [] as Entity[], - hasFocus: false + inputSources: [] as Entity[] } }, @@ -96,8 +88,7 @@ export const InputComponent = defineComponent({ () => { const capturingEntity = getState(InputState).capturingEntity if ( - !executeWhenEditing || - getState(EngineState).isEditing || + (!executeWhenEditing && getState(EngineState).isEditing) || (capturingEntity && !isAncestor(capturingEntity, entity, true)) ) return @@ -191,11 +182,18 @@ export const InputComponent = defineComponent({ useHasFocus() { const entity = useEntityContext() - const hasFocus = useHookstate(false) - InputComponent.useExecuteWithInput(() => { - const inputSources = InputComponent.getInputSourceEntities(entity) - hasFocus.set(inputSources.length > 0) - }, true) + const hasFocus = useHookstate(() => { + return InputComponent.getInputSourceEntities(entity).length > 0 + }) + useExecute( + () => { + const inputSources = InputComponent.getInputSourceEntities(entity) + hasFocus.set(inputSources.length > 0) + }, + // we want to evaluate input sources after the input system group has run, after all input systems + // have had a chance to respond to input and/or capture input sources + { after: InputSystemGroup } + ) return hasFocus }, @@ -238,14 +236,6 @@ export const InputComponent = defineComponent({ // // collider.collisionLayer.set(collider.collisionLayer.value | CollisionGroups.Input) // }, []) - useExecute( - () => { - const inputComponent = getMutableComponent(entity, InputComponent) - if (!inputComponent) return - inputComponent.hasFocus.set(inputComponent.inputSources.value.length > 0) - }, - { with: InputSystemGroup } - ) /** @todo - fix */ // useLayoutEffect(() => { // if (!input.inputSources.length || !input.grow.value) return diff --git a/packages/spatial/src/input/systems/FlyControlSystem.ts b/packages/spatial/src/input/systems/FlyControlSystem.ts index 213cbd5740..206d62f5fd 100644 --- a/packages/spatial/src/input/systems/FlyControlSystem.ts +++ b/packages/spatial/src/input/systems/FlyControlSystem.ts @@ -31,6 +31,7 @@ import { ECSState, Entity, getComponent, + getOptionalComponent, hasComponent, InputSystemGroup, removeComponent, @@ -102,8 +103,8 @@ const execute = () => { movement.copy(Vector3_Zero) for (const inputSourceEntity of inputSourceEntities) { const inputSource = getComponent(inputSourceEntity, InputSourceComponent) - const pointer = getComponent(inputSourceEntity, InputPointerComponent) - if (inputSource.buttons.SecondaryClick?.pressed) { + const pointer = getOptionalComponent(inputSourceEntity, InputPointerComponent) + if (pointer && inputSource.buttons.SecondaryClick?.pressed) { movement.x += pointer.movement.x movement.y += pointer.movement.y } diff --git a/packages/spatial/src/physics/components/RigidBodyComponent.ts b/packages/spatial/src/physics/components/RigidBodyComponent.ts index f5c944351c..2ce3a17960 100644 --- a/packages/spatial/src/physics/components/RigidBodyComponent.ts +++ b/packages/spatial/src/physics/components/RigidBodyComponent.ts @@ -24,7 +24,6 @@ Ethereal Engine. All Rights Reserved. */ import { Types } from 'bitecs' -import { useEffect, useLayoutEffect } from 'react' import { useEntityContext } from '@etherealengine/ecs' import { @@ -34,7 +33,8 @@ import { useComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { getState } from '@etherealengine/hyperflux' +import { World } from '@dimforge/rapier3d-compat' +import { useImmediateEffect, useMutableState } from '@etherealengine/hyperflux' import { proxifyQuaternion, proxifyVector3 } from '../../common/proxies/createThreejsProxy' import { Physics } from '../classes/Physics' import { PhysicsState } from '../state/PhysicsState' @@ -108,16 +108,18 @@ export const RigidBodyComponent = defineComponent({ reactor: function () { const entity = useEntityContext() const component = useComponent(entity, RigidBodyComponent) + const physicsWorld = useMutableState(PhysicsState).physicsWorld - useEffect(() => { - const physicsWorld = getState(PhysicsState).physicsWorld - Physics.createRigidBody(entity, physicsWorld) + useImmediateEffect(() => { + const world = physicsWorld.value as World + if (!world) return + Physics.createRigidBody(entity, world) return () => { - Physics.removeRigidbody(entity, physicsWorld) + Physics.removeRigidbody(entity, world) } - }, []) + }, [physicsWorld]) - useLayoutEffect(() => { + useImmediateEffect(() => { const type = component.type.value setComponent(entity, getTagComponentForRigidBody(type)) Physics.setRigidBodyType(entity, type) @@ -126,15 +128,15 @@ export const RigidBodyComponent = defineComponent({ } }, [component.type]) - useLayoutEffect(() => { + useImmediateEffect(() => { Physics.enabledCcd(entity, component.ccd.value) }, [component.ccd]) - useLayoutEffect(() => { + useImmediateEffect(() => { Physics.lockRotations(entity, !component.allowRolling.value) }, [component.allowRolling]) - useLayoutEffect(() => { + useImmediateEffect(() => { Physics.setEnabledRotations(entity, component.enabledRotations.value as [boolean, boolean, boolean]) }, [component.enabledRotations]) diff --git a/packages/spatial/src/renderer/components/MeshComponent.ts b/packages/spatial/src/renderer/components/MeshComponent.ts index 9f818139ce..a889bc7c69 100644 --- a/packages/spatial/src/renderer/components/MeshComponent.ts +++ b/packages/spatial/src/renderer/components/MeshComponent.ts @@ -108,6 +108,7 @@ export function useMeshComponent { const mesh = meshComponent.value as Mesh addObjectToGroup(entity, mesh) diff --git a/packages/spatial/src/transform/components/EntityTree.tsx b/packages/spatial/src/transform/components/EntityTree.tsx index e6d6222826..2bef32ec50 100644 --- a/packages/spatial/src/transform/components/EntityTree.tsx +++ b/packages/spatial/src/transform/components/EntityTree.tsx @@ -23,8 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import React, { useLayoutEffect } from 'react' - import { ComponentType, defineComponent, @@ -39,7 +37,8 @@ import { } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity, UndefinedEntity } from '@etherealengine/ecs/src/Entity' import { entityExists, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' -import { NO_PROXY, none, startReactor, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, none, startReactor, useHookstate, useImmediateEffect } from '@etherealengine/hyperflux' +import React, { useLayoutEffect } from 'react' import { SceneComponent } from '../../renderer/components/SceneComponents' import { TransformComponent } from './TransformComponent' @@ -399,7 +398,7 @@ export function useTreeQuery(entity: Entity) { export function useAncestorWithComponent(entity: Entity, component: ComponentType) { const result = useHookstate(UndefinedEntity) - useLayoutEffect(() => { + useImmediateEffect(() => { let unmounted = false const ParentSubReactor = (props: { entity: Entity }) => { const tree = useOptionalComponent(props.entity, EntityTreeComponent)