Skip to content

Commit 3d4864b

Browse files
committed
🐛 react: useTrait always re-renders with entity.change() (#210)
1 parent d61a320 commit 3d4864b

File tree

2 files changed

+618
-528
lines changed

2 files changed

+618
-528
lines changed
Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,70 @@
11
import { $internal, type Entity, type Trait, type TraitRecord, type World } from '@koota/core';
2-
import { useEffect, useMemo, useState } from 'react';
2+
import { useEffect, useMemo, useReducer, useRef } from 'react';
33
import { isWorld } from '../utils/is-world';
44
import { useWorld } from '../world/use-world';
55

66
export function useTrait<T extends Trait>(
7-
target: Entity | World | undefined | null,
8-
trait: T
7+
target: Entity | World | undefined | null,
8+
trait: T
99
): TraitRecord<T> | undefined {
10-
// Get the world from context -- it may be used.
11-
// Note: With React 19 we can get it with use conditionally.
12-
const contextWorld = useWorld();
10+
const contextWorld = useWorld();
11+
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
12+
const valueRef = useRef<TraitRecord<T> | undefined>(undefined);
13+
const memoRef = useRef<ReturnType<typeof createSubscriptions<T>> | undefined>(undefined);
1314

14-
// Memoize the target entity and a subscriber function.
15-
// If the target is undefined or null, undefined is returned here so the hook can exit early.
16-
const memo = useMemo(
17-
() => (target ? createSubscriptions(target, trait, contextWorld) : undefined),
18-
[target, trait, contextWorld]
19-
);
15+
const memo = useMemo(
16+
() => (target ? createSubscriptions(target, trait, contextWorld) : undefined),
17+
[target, trait, contextWorld]
18+
);
2019

21-
// Initialize the state with the current value of the trait.
22-
const [value, setValue] = useState<TraitRecord<T> | undefined>(() => {
23-
return memo?.entity.has(trait) ? memo?.entity.get(trait) : undefined;
24-
});
20+
// Update cached value when the target or trait changes
21+
if (memoRef.current !== memo) {
22+
memoRef.current = memo;
23+
valueRef.current = memo?.entity.has(trait) ? memo.entity.get(trait) : undefined;
24+
}
2525

26-
// Subscribe to changes in the trait.
27-
useEffect(() => {
28-
if (!memo) {
29-
setValue(undefined);
30-
return;
31-
}
32-
const unsubscribe = memo.subscribe(setValue);
33-
return () => unsubscribe();
34-
}, [memo]);
26+
useEffect(() => {
27+
if (!memo) return;
3528

36-
return value;
29+
const unsub = memo.subscribe((value) => {
30+
valueRef.current = value;
31+
forceUpdate();
32+
});
33+
34+
return () => unsub();
35+
}, [memo]);
36+
37+
return valueRef.current;
3738
}
3839

3940
function createSubscriptions<T extends Trait>(target: Entity | World, trait: T, contextWorld: World) {
40-
const world = isWorld(target) ? target : contextWorld;
41-
const entity = isWorld(target) ? target[$internal].worldEntity : target;
41+
// Use the context world unless the target is a world itself
42+
const world = isWorld(target) ? target : contextWorld;
43+
const entity = isWorld(target) ? target[$internal].worldEntity : target;
4244

43-
return {
44-
entity,
45-
subscribe: (setValue: (value: TraitRecord<T> | undefined) => void) => {
46-
const onChangeUnsub = world.onChange(trait, (e) => {
47-
if (e === entity) setValue(e.get(trait));
48-
});
45+
return {
46+
entity,
47+
subscribe: (setValue: (value: TraitRecord<T> | undefined) => void) => {
48+
const onChangeUnsub = world.onChange(trait, (e) => {
49+
if (e === entity) setValue(e.get(trait));
50+
});
4951

50-
const onAddUnsub = world.onAdd(trait, (e) => {
51-
if (e === entity) setValue(e.get(trait));
52-
});
52+
const onAddUnsub = world.onAdd(trait, (e) => {
53+
if (e === entity) setValue(e.get(trait));
54+
});
5355

54-
const onRemoveUnsub = world.onRemove(trait, (e) => {
55-
if (e === entity) setValue(undefined);
56-
});
56+
const onRemoveUnsub = world.onRemove(trait, (e) => {
57+
if (e === entity) setValue(undefined);
58+
});
5759

58-
setValue(entity.has(trait) ? entity.get(trait) : undefined);
60+
// Set initial value
61+
setValue(entity.has(trait) ? entity.get(trait) : undefined);
5962

60-
return () => {
61-
onChangeUnsub();
62-
onAddUnsub();
63-
onRemoveUnsub();
64-
};
65-
},
66-
};
63+
return () => {
64+
onChangeUnsub();
65+
onAddUnsub();
66+
onRemoveUnsub();
67+
};
68+
},
69+
};
6770
}

0 commit comments

Comments
 (0)