@latticexyz/react 2.0.0-alpha.1.78 → 2.0.0-alpha.1.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/index.ts +4 -0
- package/src/useComponentValue.test.ts +88 -0
- package/src/useComponentValue.ts +48 -0
- package/src/useDeprecatedComputedValue.ts +14 -0
- package/src/useEntityQuery.test.ts +163 -0
- package/src/useEntityQuery.ts +37 -0
- package/src/useObservableValue.ts +17 -0
- package/src/utils/useDeepMemo.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latticexyz/react",
|
|
3
|
-
"version": "2.0.0-alpha.1.
|
|
3
|
+
"version": "2.0.0-alpha.1.80+943e962f",
|
|
4
4
|
"description": "React tools for MUD client.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "tsc --noEmit && jest"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@latticexyz/recs": "2.0.0-alpha.1.
|
|
26
|
+
"@latticexyz/recs": "2.0.0-alpha.1.80+943e962f",
|
|
27
27
|
"fast-deep-equal": "^3.1.3",
|
|
28
28
|
"mobx": "^6.7.0",
|
|
29
29
|
"react": "^18.2.0",
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"typedoc": "0.23.21",
|
|
44
44
|
"typedoc-plugin-markdown": "^3.13.6"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "943e962f0380d629612c4b843d4dd22b9996c41f"
|
|
47
47
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-hooks";
|
|
2
|
+
import {
|
|
3
|
+
World,
|
|
4
|
+
Type,
|
|
5
|
+
createWorld,
|
|
6
|
+
defineComponent,
|
|
7
|
+
Component,
|
|
8
|
+
createEntity,
|
|
9
|
+
withValue,
|
|
10
|
+
setComponent,
|
|
11
|
+
removeComponent,
|
|
12
|
+
} from "@latticexyz/recs";
|
|
13
|
+
import { useComponentValue } from "./useComponentValue";
|
|
14
|
+
|
|
15
|
+
describe("useComponentValue", () => {
|
|
16
|
+
let world: World;
|
|
17
|
+
let Position: Component<{
|
|
18
|
+
x: Type.Number;
|
|
19
|
+
y: Type.Number;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
world = createWorld();
|
|
24
|
+
Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should return Position value for entity", () => {
|
|
28
|
+
const entity = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
29
|
+
|
|
30
|
+
const { result } = renderHook(() => useComponentValue(Position, entity));
|
|
31
|
+
expect(result.current).toEqual({ x: 1, y: 1 });
|
|
32
|
+
|
|
33
|
+
act(() => {
|
|
34
|
+
setComponent(Position, entity, { x: 0, y: 0 });
|
|
35
|
+
});
|
|
36
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
37
|
+
|
|
38
|
+
act(() => {
|
|
39
|
+
removeComponent(Position, entity);
|
|
40
|
+
});
|
|
41
|
+
expect(result.current).toBe(undefined);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should re-render only when Position changes for entity", () => {
|
|
45
|
+
const entity = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
46
|
+
const otherEntity = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
47
|
+
|
|
48
|
+
const { result } = renderHook(() => useComponentValue(Position, entity));
|
|
49
|
+
expect(result.all.length).toBe(2);
|
|
50
|
+
expect(result.current).toEqual({ x: 1, y: 1 });
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
setComponent(Position, entity, { x: 0, y: 0 });
|
|
54
|
+
});
|
|
55
|
+
expect(result.all.length).toBe(3);
|
|
56
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
57
|
+
|
|
58
|
+
act(() => {
|
|
59
|
+
setComponent(Position, otherEntity, { x: 0, y: 0 });
|
|
60
|
+
removeComponent(Position, otherEntity);
|
|
61
|
+
});
|
|
62
|
+
expect(result.all.length).toBe(3);
|
|
63
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
64
|
+
|
|
65
|
+
act(() => {
|
|
66
|
+
removeComponent(Position, entity);
|
|
67
|
+
});
|
|
68
|
+
expect(result.all.length).toBe(4);
|
|
69
|
+
expect(result.current).toBe(undefined);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return default value when Position is not set", () => {
|
|
73
|
+
const entity = createEntity(world);
|
|
74
|
+
|
|
75
|
+
const { result } = renderHook(() => useComponentValue(Position, entity, { x: -1, y: -1 }));
|
|
76
|
+
expect(result.current).toEqual({ x: -1, y: -1 });
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
setComponent(Position, entity, { x: 0, y: 0 });
|
|
80
|
+
});
|
|
81
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
82
|
+
|
|
83
|
+
act(() => {
|
|
84
|
+
removeComponent(Position, entity);
|
|
85
|
+
});
|
|
86
|
+
expect(result.current).toEqual({ x: -1, y: -1 });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ComponentValue,
|
|
4
|
+
defineQuery,
|
|
5
|
+
Entity,
|
|
6
|
+
getComponentValue,
|
|
7
|
+
Has,
|
|
8
|
+
isComponentUpdate,
|
|
9
|
+
Metadata,
|
|
10
|
+
Schema,
|
|
11
|
+
} from "@latticexyz/recs";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
|
|
14
|
+
export function useComponentValue<S extends Schema>(
|
|
15
|
+
component: Component<S, Metadata, undefined>,
|
|
16
|
+
entity: Entity | undefined,
|
|
17
|
+
defaultValue: ComponentValue<S>
|
|
18
|
+
): ComponentValue<S>;
|
|
19
|
+
|
|
20
|
+
export function useComponentValue<S extends Schema>(
|
|
21
|
+
component: Component<S, Metadata, undefined>,
|
|
22
|
+
entity: Entity | undefined
|
|
23
|
+
): ComponentValue<S> | undefined;
|
|
24
|
+
|
|
25
|
+
export function useComponentValue<S extends Schema>(
|
|
26
|
+
component: Component<S, Metadata, undefined>,
|
|
27
|
+
entity: Entity | undefined,
|
|
28
|
+
defaultValue?: ComponentValue<S>
|
|
29
|
+
) {
|
|
30
|
+
const [value, setValue] = useState(entity != null ? getComponentValue(component, entity) : undefined);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
// component or entity changed, update state to latest value
|
|
34
|
+
setValue(entity != null ? getComponentValue(component, entity) : undefined);
|
|
35
|
+
if (entity == null) return;
|
|
36
|
+
|
|
37
|
+
const queryResult = defineQuery([Has(component)], { runOnInit: false });
|
|
38
|
+
const subscription = queryResult.update$.subscribe((update) => {
|
|
39
|
+
if (isComponentUpdate(update, component) && update.entity === entity) {
|
|
40
|
+
const [nextValue] = update.value;
|
|
41
|
+
setValue(nextValue);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return () => subscription.unsubscribe();
|
|
45
|
+
}, [component, entity]);
|
|
46
|
+
|
|
47
|
+
return value ?? defaultValue;
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IComputedValue } from "mobx";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
/** @deprecated See https://github.com/latticexyz/mud/issues/339 */
|
|
5
|
+
export const useDeprecatedComputedValue = <T>(computedValue: IComputedValue<T> & { observe_: any }) => {
|
|
6
|
+
const [value, setValue] = useState<T>(computedValue.get());
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const unsubscribe = computedValue.observe_(() => setValue(computedValue.get()));
|
|
10
|
+
return () => unsubscribe();
|
|
11
|
+
}, [computedValue]);
|
|
12
|
+
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-hooks";
|
|
2
|
+
import {
|
|
3
|
+
World,
|
|
4
|
+
Type,
|
|
5
|
+
createWorld,
|
|
6
|
+
defineComponent,
|
|
7
|
+
Component,
|
|
8
|
+
createEntity,
|
|
9
|
+
withValue,
|
|
10
|
+
Has,
|
|
11
|
+
setComponent,
|
|
12
|
+
HasValue,
|
|
13
|
+
removeComponent,
|
|
14
|
+
} from "@latticexyz/recs";
|
|
15
|
+
import { useEntityQuery } from "./useEntityQuery";
|
|
16
|
+
|
|
17
|
+
describe("useEntityQuery", () => {
|
|
18
|
+
let world: World;
|
|
19
|
+
let Position: Component<{
|
|
20
|
+
x: Type.Number;
|
|
21
|
+
y: Type.Number;
|
|
22
|
+
}>;
|
|
23
|
+
let OwnedBy: Component<{ value: Type.Entity }>;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
world = createWorld();
|
|
27
|
+
Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" });
|
|
28
|
+
OwnedBy = defineComponent(world, { value: Type.Entity }, { id: "OwnedBy" });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should find entities with Position component", () => {
|
|
32
|
+
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
33
|
+
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
34
|
+
const entity3 = createEntity(world, []);
|
|
35
|
+
|
|
36
|
+
const { result } = renderHook(() => useEntityQuery([Has(Position)], { updateOnValueChange: false }));
|
|
37
|
+
const { result: resultOnValueChange } = renderHook(() =>
|
|
38
|
+
useEntityQuery([Has(Position)], { updateOnValueChange: true })
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(result.current.length).toBe(2);
|
|
42
|
+
expect(result.current).toContain(entity1);
|
|
43
|
+
expect(result.current).toContain(entity2);
|
|
44
|
+
expect(result.current).not.toContain(entity3);
|
|
45
|
+
expect(resultOnValueChange.current).toEqual(result.current);
|
|
46
|
+
|
|
47
|
+
act(() => {
|
|
48
|
+
setComponent(Position, entity3, { x: 0, y: 0 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.current.length).toBe(3);
|
|
52
|
+
expect(result.current).toContain(entity1);
|
|
53
|
+
expect(result.current).toContain(entity2);
|
|
54
|
+
expect(result.current).toContain(entity3);
|
|
55
|
+
expect(resultOnValueChange.current).toEqual(result.current);
|
|
56
|
+
|
|
57
|
+
act(() => {
|
|
58
|
+
removeComponent(Position, entity1);
|
|
59
|
+
removeComponent(Position, entity3);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.current.length).toBe(1);
|
|
63
|
+
expect(result.current).not.toContain(entity1);
|
|
64
|
+
expect(result.current).toContain(entity2);
|
|
65
|
+
expect(result.current).not.toContain(entity3);
|
|
66
|
+
expect(resultOnValueChange.current).toEqual(result.current);
|
|
67
|
+
|
|
68
|
+
act(() => {
|
|
69
|
+
removeComponent(Position, entity2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.current.length).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should re-render only when Position changes", () => {
|
|
76
|
+
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
77
|
+
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
78
|
+
const entity3 = createEntity(world, []);
|
|
79
|
+
|
|
80
|
+
const { result } = renderHook(() => useEntityQuery([Has(Position)], { updateOnValueChange: false }));
|
|
81
|
+
const { result: resultOnValueChange } = renderHook(() =>
|
|
82
|
+
useEntityQuery([Has(Position)], { updateOnValueChange: true })
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(result.all).toHaveLength(2);
|
|
86
|
+
expect(result.current).toHaveLength(2);
|
|
87
|
+
expect(result.current).toContain(entity1);
|
|
88
|
+
expect(result.current).toContain(entity2);
|
|
89
|
+
expect(result.current).not.toContain(entity3);
|
|
90
|
+
|
|
91
|
+
// Changing an entity's component value should NOT re-render,
|
|
92
|
+
// unless updateOnValueChange === true
|
|
93
|
+
act(() => {
|
|
94
|
+
setComponent(Position, entity2, { x: 0, y: 0 });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(result.all).toHaveLength(2);
|
|
98
|
+
expect(resultOnValueChange.all).toHaveLength(3);
|
|
99
|
+
|
|
100
|
+
// Changing a different component value should NOT re-render
|
|
101
|
+
act(() => {
|
|
102
|
+
setComponent(OwnedBy, entity2, { value: entity1 });
|
|
103
|
+
setComponent(OwnedBy, entity3, { value: entity1 });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.all).toHaveLength(2);
|
|
107
|
+
expect(resultOnValueChange.all).toHaveLength(3);
|
|
108
|
+
|
|
109
|
+
// Changing which entities have the component should re-render
|
|
110
|
+
act(() => {
|
|
111
|
+
setComponent(Position, entity3, { x: 0, y: 0 });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.all).toHaveLength(3);
|
|
115
|
+
expect(resultOnValueChange.all).toHaveLength(4);
|
|
116
|
+
expect(result.current).toHaveLength(3);
|
|
117
|
+
expect(result.current).toContain(entity1);
|
|
118
|
+
expect(result.current).toContain(entity2);
|
|
119
|
+
expect(result.current).toContain(entity3);
|
|
120
|
+
|
|
121
|
+
// Changing which entities have the component should re-render
|
|
122
|
+
act(() => {
|
|
123
|
+
removeComponent(Position, entity1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(result.all).toHaveLength(4);
|
|
127
|
+
expect(resultOnValueChange.all).toHaveLength(5);
|
|
128
|
+
expect(result.current).toHaveLength(2);
|
|
129
|
+
expect(result.current).toContain(entity2);
|
|
130
|
+
expect(result.current).toContain(entity3);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should re-render as hook arguments change", () => {
|
|
134
|
+
// TODO: reduce re-renders during argument changes?
|
|
135
|
+
|
|
136
|
+
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
137
|
+
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
138
|
+
const entity3 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
139
|
+
|
|
140
|
+
const { result, rerender } = renderHook(({ x, y }) => useEntityQuery([HasValue(Position, { x, y })]), {
|
|
141
|
+
initialProps: { x: 1, y: 1 },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.all).toHaveLength(2);
|
|
145
|
+
expect(result.current).toHaveLength(1);
|
|
146
|
+
expect(result.current).toContain(entity1);
|
|
147
|
+
|
|
148
|
+
rerender({ x: 1, y: 1 });
|
|
149
|
+
expect(result.all).toHaveLength(3);
|
|
150
|
+
expect(result.current).toHaveLength(1);
|
|
151
|
+
expect(result.current).toContain(entity1);
|
|
152
|
+
|
|
153
|
+
rerender({ x: 2, y: 2 });
|
|
154
|
+
expect(result.all).toHaveLength(6);
|
|
155
|
+
expect(result.current).toHaveLength(2);
|
|
156
|
+
expect(result.current).toContain(entity2);
|
|
157
|
+
expect(result.current).toContain(entity3);
|
|
158
|
+
|
|
159
|
+
rerender({ x: -1, y: -1 });
|
|
160
|
+
expect(result.all).toHaveLength(9);
|
|
161
|
+
expect(result.current).toHaveLength(0);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineQuery, QueryFragment } from "@latticexyz/recs";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useDeepMemo } from "./utils/useDeepMemo";
|
|
4
|
+
import isEqual from "fast-deep-equal";
|
|
5
|
+
import { distinctUntilChanged, map } from "rxjs";
|
|
6
|
+
|
|
7
|
+
// This does a little more rendering than is necessary when arguments change,
|
|
8
|
+
// but at least it's giving correct results now. Will optimize later!
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns all matching entities for a given entity query,
|
|
12
|
+
* and triggers a re-render as new query results come in.
|
|
13
|
+
*
|
|
14
|
+
* @param fragments Query fragments to match against, executed from left to right.
|
|
15
|
+
* @param options.updateOnValueChange False - re-renders only on entity array changes. True (default) - also on component value changes.
|
|
16
|
+
* @returns Set of entities matching the query fragments.
|
|
17
|
+
*/
|
|
18
|
+
export function useEntityQuery(fragments: QueryFragment[], options?: { updateOnValueChange?: boolean }) {
|
|
19
|
+
const updateOnValueChange = options?.updateOnValueChange ?? true;
|
|
20
|
+
|
|
21
|
+
const stableFragments = useDeepMemo(fragments);
|
|
22
|
+
const query = useMemo(() => defineQuery(stableFragments, { runOnInit: true }), [stableFragments]);
|
|
23
|
+
const [entities, setEntities] = useState([...query.matching]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setEntities([...query.matching]);
|
|
27
|
+
let observable = query.update$.pipe(map(() => [...query.matching]));
|
|
28
|
+
if (!updateOnValueChange) {
|
|
29
|
+
// re-render only on entity array changes
|
|
30
|
+
observable = observable.pipe(distinctUntilChanged((a, b) => isEqual(a, b)));
|
|
31
|
+
}
|
|
32
|
+
const subscription = observable.subscribe((entities) => setEntities(entities));
|
|
33
|
+
return () => subscription.unsubscribe();
|
|
34
|
+
}, [query, updateOnValueChange]);
|
|
35
|
+
|
|
36
|
+
return entities;
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
|
|
4
|
+
export function useObservableValue<T>(observable: Observable<T>, defaultValue: T): T;
|
|
5
|
+
|
|
6
|
+
export function useObservableValue<T>(observable: Observable<T>): T | undefined;
|
|
7
|
+
|
|
8
|
+
export function useObservableValue<T>(observable: Observable<T>, defaultValue?: T) {
|
|
9
|
+
const [value, setValue] = useState(defaultValue);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const subscription = observable.subscribe(setValue);
|
|
13
|
+
return () => subscription.unsubscribe();
|
|
14
|
+
}, [observable]);
|
|
15
|
+
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import isEqual from "fast-deep-equal";
|
|
3
|
+
|
|
4
|
+
export const useDeepMemo = <T>(currentValue: T): T => {
|
|
5
|
+
const [stableValue, setStableValue] = useState(currentValue);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!isEqual(currentValue, stableValue)) {
|
|
9
|
+
setStableValue(currentValue);
|
|
10
|
+
}
|
|
11
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
12
|
+
}, [currentValue]);
|
|
13
|
+
|
|
14
|
+
return stableValue;
|
|
15
|
+
};
|