@latticexyz/react 0.0.0-indexing-20230707130424
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/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/index.ts +5 -0
- package/src/store-cache/index.ts +2 -0
- package/src/store-cache/useRow.test.ts +197 -0
- package/src/store-cache/useRow.ts +23 -0
- package/src/store-cache/useRows.test.ts +168 -0
- package/src/store-cache/useRows.ts +32 -0
- package/src/useComponentValue.test.ts +89 -0
- package/src/useComponentValue.ts +48 -0
- package/src/useDeprecatedComputedValue.ts +14 -0
- package/src/useEntityQuery.test.ts +164 -0
- package/src/useEntityQuery.ts +37 -0
- package/src/useObservableValue.ts +17 -0
- package/src/utils/useDeepMemo.ts +15 -0
- package/src/utils/useMountedState.ts +23 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
## 0.0.0-indexing-20230707130424
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`904fd7d4`](https://github.com/latticexyz/mud/commit/904fd7d4ee06a86e481e3e02fd5744224376d0c9)]:
|
|
8
|
+
- @latticexyz/store@0.0.0-indexing-20230707130424
|
|
9
|
+
- @latticexyz/store-cache@0.0.0-indexing-20230707130424
|
|
10
|
+
|
|
11
|
+
All notable changes to this project will be documented in this file.
|
|
12
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
13
|
+
|
|
14
|
+
# [1.42.0](https://github.com/latticexyz/mud/compare/v1.41.0...v1.42.0) (2023-04-13)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- v2 event decoding ([#415](https://github.com/latticexyz/mud/issues/415)) ([374ed54](https://github.com/latticexyz/mud/commit/374ed542c3387a4ec2b36ab68ae534419aa58763))
|
|
19
|
+
|
|
20
|
+
# [1.41.0](https://github.com/latticexyz/mud/compare/v1.40.0...v1.41.0) (2023-03-09)
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
- **react:** option for useEntityQuery to re-render on value changes ([#460](https://github.com/latticexyz/mud/issues/460)) ([6b90b85](https://github.com/latticexyz/mud/commit/6b90b85febe00ff0a2c9a3c4642d0197b5107e35))
|
|
25
|
+
|
|
26
|
+
# [1.40.0](https://github.com/latticexyz/mud/compare/v1.39.0...v1.40.0) (2023-03-03)
|
|
27
|
+
|
|
28
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
29
|
+
|
|
30
|
+
# [1.39.0](https://github.com/latticexyz/mud/compare/v1.38.0...v1.39.0) (2023-02-22)
|
|
31
|
+
|
|
32
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
33
|
+
|
|
34
|
+
# [1.38.0](https://github.com/latticexyz/mud/compare/v1.37.1...v1.38.0) (2023-02-22)
|
|
35
|
+
|
|
36
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
37
|
+
|
|
38
|
+
## [1.37.1](https://github.com/latticexyz/mud/compare/v1.37.0...v1.37.1) (2023-02-17)
|
|
39
|
+
|
|
40
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
41
|
+
|
|
42
|
+
# [1.37.0](https://github.com/latticexyz/mud/compare/v1.36.1...v1.37.0) (2023-02-16)
|
|
43
|
+
|
|
44
|
+
### Bug Fixes
|
|
45
|
+
|
|
46
|
+
- package entry points, peer dep versions ([#409](https://github.com/latticexyz/mud/issues/409)) ([66a7fd6](https://github.com/latticexyz/mud/commit/66a7fd6f74620ce02c60e3d55701d4740cc65251))
|
|
47
|
+
|
|
48
|
+
### Reverts
|
|
49
|
+
|
|
50
|
+
- Revert "chore(release): publish v1.37.0" ([c934f53](https://github.com/latticexyz/mud/commit/c934f5388c1e56f2fe6390fdda30f5b9b1ea1c20))
|
|
51
|
+
|
|
52
|
+
## [1.36.1](https://github.com/latticexyz/mud/compare/v1.36.0...v1.36.1) (2023-02-16)
|
|
53
|
+
|
|
54
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
55
|
+
|
|
56
|
+
# [1.35.0](https://github.com/latticexyz/mud/compare/v1.34.0...v1.35.0) (2023-02-15)
|
|
57
|
+
|
|
58
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
59
|
+
|
|
60
|
+
# [1.34.0](https://github.com/latticexyz/mud/compare/v1.33.1...v1.34.0) (2023-01-29)
|
|
61
|
+
|
|
62
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
63
|
+
|
|
64
|
+
## [1.33.1](https://github.com/latticexyz/mud/compare/v1.33.0...v1.33.1) (2023-01-12)
|
|
65
|
+
|
|
66
|
+
**Note:** Version bump only for package @latticexyz/react
|
|
67
|
+
|
|
68
|
+
# [1.33.0](https://github.com/latticexyz/mud/compare/v1.32.0...v1.33.0) (2023-01-12)
|
|
69
|
+
|
|
70
|
+
### Features
|
|
71
|
+
|
|
72
|
+
- **react:** add react package ([#294](https://github.com/latticexyz/mud/issues/294)) ([f5ee290](https://github.com/latticexyz/mud/commit/f5ee290e776276b2b0dd273705694df04a85f400))
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-present Lattice Labs Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# MUD React
|
|
2
|
+
|
|
3
|
+
React hooks (and more) for building MUD clients.
|
|
4
|
+
|
|
5
|
+
## Hooks for `store-cache`
|
|
6
|
+
|
|
7
|
+
### useRows
|
|
8
|
+
|
|
9
|
+
Returns an array of all rows matching the provided filter. Re-renders if the filter changes or if table entries matching the provided filter change.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// get a list of all entries in the database
|
|
13
|
+
const allRows = useRows(storeCache);
|
|
14
|
+
// -> [ { namespace: "", table: "Position", key: { key: "0x01" }, value: { x: 1, y: 2 } }, ... ]
|
|
15
|
+
|
|
16
|
+
// get a list of all entries in the Position table
|
|
17
|
+
const allPositionRows = useRows(storeCache, { table: "Position" });
|
|
18
|
+
|
|
19
|
+
// get a list of all entries in the position table with key greater than `0x0A`
|
|
20
|
+
const filteredRows = useRows(storeCache, { table: "Position", key: { gt: { key: "0x0A" } } });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### useRow
|
|
24
|
+
|
|
25
|
+
Returns a single row with the provided key in the provided table. Re-renders if the filter changes or if the value of this row changes.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// get the Position value of key `0x01`
|
|
29
|
+
const position = useRow(storeCache, { table: "Position", key: { key: "0x01" } });
|
|
30
|
+
// -> { namespace: "", table: "Position", key: { key: "0x01" }, value: { x: 1, y: 2 } }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Hooks for `recs`
|
|
34
|
+
|
|
35
|
+
### useComponentValue
|
|
36
|
+
|
|
37
|
+
Returns the value of the component for the entity, and triggers a re-render as the component is added/removed/updated.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const position = useComponentValue(Position, entity);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### useEntityQuery
|
|
44
|
+
|
|
45
|
+
Returns all matching `EntityIndex`es for a given entity query, and triggers a re-render as new query results come in.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const entities = useEntityQuery([Has(Position)]);
|
|
49
|
+
const playersAtPosition = useEntityQuery([Has(Player), HasValue(Position, { x: 0, y: 0 })]);
|
|
50
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{defineQuery as d,getComponentValue as p,Has as C,isComponentUpdate as S}from"@latticexyz/recs";import{useEffect as T,useState as x}from"react";function G(e,t,s){let[o,n]=x(t!=null?p(e,t):void 0);return T(()=>{if(n(t!=null?p(e,t):void 0),t==null)return;let a=d([C(e)],{runOnInit:!1}).update$.subscribe(u=>{if(S(u,e)&&u.entity===t){let[f]=u.value;n(f)}});return()=>a.unsubscribe()},[e,t]),o??s}import{useEffect as g,useState as y}from"react";var N=e=>{let[t,s]=y(e.get());return g(()=>{let o=e.observe_(()=>s(e.get()));return()=>o()},[e]),t};import{defineQuery as E}from"@latticexyz/recs";import{useEffect as R,useMemo as h,useState as M}from"react";import{useEffect as V,useState as O}from"react";import v from"fast-deep-equal";var i=e=>{let[t,s]=O(e);return V(()=>{v(e,t)||s(e)},[e]),t};import D from"fast-deep-equal";import{distinctUntilChanged as k,map as w}from"rxjs";function ue(e,t){let s=t?.updateOnValueChange??!0,o=i(e),n=h(()=>E(o,{runOnInit:!0}),[o]),[r,a]=M([...n.matching]);return R(()=>{a([...n.matching]);let u=n.update$.pipe(w(()=>[...n.matching]));s||(u=u.pipe(k((m,b)=>D(m,b))));let f=u.subscribe(m=>a(m));return()=>f.unsubscribe()},[n,s]),r}import{useEffect as F,useState as q}from"react";function fe(e,t){let[s,o]=q(t);return F(()=>{let n=e.subscribe(o);return()=>n.unsubscribe()},[e]),s}import{useEffect as K}from"react";import{useCallback as I,useEffect as Q,useRef as U,useState as A}from"react";function l(e){let[t,s]=A(e),o=U(!1);Q(()=>(o.current=!0,()=>{o.current=!1}));let n=I((...r)=>{o.current?s(...r):console.warn("Ignoring `setState` call because component unmounted",...r)},[]);return[t,n]}function c(e,t){let[s,o]=l([]),n=i(t);return K(()=>{e.scan(n).then(o);let r=e.subscribe(()=>{e.scan(n).then(o)},n);return()=>{r.then(a=>a())}},[n,o,e]),s}function ye(e,t){let{namespace:s,table:o,key:n}=t;return c(e,{namespace:s,table:o,key:{eq:n}})[0]}export{G as useComponentValue,N as useDeprecatedComputedValue,ue as useEntityQuery,fe as useObservableValue,ye as useRow,c as useRows};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useComponentValue.ts","../src/useDeprecatedComputedValue.ts","../src/useEntityQuery.ts","../src/utils/useDeepMemo.ts","../src/useObservableValue.ts","../src/store-cache/useRows.ts","../src/utils/useMountedState.ts","../src/store-cache/useRow.ts"],"sourcesContent":["import {\n Component,\n ComponentValue,\n defineQuery,\n Entity,\n getComponentValue,\n Has,\n isComponentUpdate,\n Metadata,\n Schema,\n} from \"@latticexyz/recs\";\nimport { useEffect, useState } from \"react\";\n\nexport function useComponentValue<S extends Schema>(\n component: Component<S, Metadata, undefined>,\n entity: Entity | undefined,\n defaultValue: ComponentValue<S>\n): ComponentValue<S>;\n\nexport function useComponentValue<S extends Schema>(\n component: Component<S, Metadata, undefined>,\n entity: Entity | undefined\n): ComponentValue<S> | undefined;\n\nexport function useComponentValue<S extends Schema>(\n component: Component<S, Metadata, undefined>,\n entity: Entity | undefined,\n defaultValue?: ComponentValue<S>\n) {\n const [value, setValue] = useState(entity != null ? getComponentValue(component, entity) : undefined);\n\n useEffect(() => {\n // component or entity changed, update state to latest value\n setValue(entity != null ? getComponentValue(component, entity) : undefined);\n if (entity == null) return;\n\n const queryResult = defineQuery([Has(component)], { runOnInit: false });\n const subscription = queryResult.update$.subscribe((update) => {\n if (isComponentUpdate(update, component) && update.entity === entity) {\n const [nextValue] = update.value;\n setValue(nextValue);\n }\n });\n return () => subscription.unsubscribe();\n }, [component, entity]);\n\n return value ?? defaultValue;\n}\n","import { IComputedValue } from \"mobx\";\nimport { useEffect, useState } from \"react\";\n\n/** @deprecated See https://github.com/latticexyz/mud/issues/339 */\nexport const useDeprecatedComputedValue = <T>(computedValue: IComputedValue<T> & { observe_: any }) => {\n const [value, setValue] = useState<T>(computedValue.get());\n\n useEffect(() => {\n const unsubscribe = computedValue.observe_(() => setValue(computedValue.get()));\n return () => unsubscribe();\n }, [computedValue]);\n\n return value;\n};\n","import { defineQuery, QueryFragment } from \"@latticexyz/recs\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useDeepMemo } from \"./utils/useDeepMemo\";\nimport isEqual from \"fast-deep-equal\";\nimport { distinctUntilChanged, map } from \"rxjs\";\n\n// This does a little more rendering than is necessary when arguments change,\n// but at least it's giving correct results now. Will optimize later!\n\n/**\n * Returns all matching entities for a given entity query,\n * and triggers a re-render as new query results come in.\n *\n * @param fragments Query fragments to match against, executed from left to right.\n * @param options.updateOnValueChange False - re-renders only on entity array changes. True (default) - also on component value changes.\n * @returns Set of entities matching the query fragments.\n */\nexport function useEntityQuery(fragments: QueryFragment[], options?: { updateOnValueChange?: boolean }) {\n const updateOnValueChange = options?.updateOnValueChange ?? true;\n\n const stableFragments = useDeepMemo(fragments);\n const query = useMemo(() => defineQuery(stableFragments, { runOnInit: true }), [stableFragments]);\n const [entities, setEntities] = useState([...query.matching]);\n\n useEffect(() => {\n setEntities([...query.matching]);\n let observable = query.update$.pipe(map(() => [...query.matching]));\n if (!updateOnValueChange) {\n // re-render only on entity array changes\n observable = observable.pipe(distinctUntilChanged((a, b) => isEqual(a, b)));\n }\n const subscription = observable.subscribe((entities) => setEntities(entities));\n return () => subscription.unsubscribe();\n }, [query, updateOnValueChange]);\n\n return entities;\n}\n","import { useEffect, useState } from \"react\";\nimport isEqual from \"fast-deep-equal\";\n\nexport const useDeepMemo = <T>(currentValue: T): T => {\n const [stableValue, setStableValue] = useState(currentValue);\n\n useEffect(() => {\n if (!isEqual(currentValue, stableValue)) {\n setStableValue(currentValue);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [currentValue]);\n\n return stableValue;\n};\n","import { useEffect, useState } from \"react\";\nimport { Observable } from \"rxjs\";\n\nexport function useObservableValue<T>(observable: Observable<T>, defaultValue: T): T;\n\nexport function useObservableValue<T>(observable: Observable<T>): T | undefined;\n\nexport function useObservableValue<T>(observable: Observable<T>, defaultValue?: T) {\n const [value, setValue] = useState(defaultValue);\n\n useEffect(() => {\n const subscription = observable.subscribe(setValue);\n return () => subscription.unsubscribe();\n }, [observable]);\n\n return value;\n}\n","import { DatabaseClient, FilterOptions, ScanResult } from \"@latticexyz/store-cache\";\nimport { StoreConfig } from \"@latticexyz/store\";\nimport { useEffect, useState } from \"react\";\nimport { useDeepMemo } from \"../utils/useDeepMemo\";\nimport { useMountedState } from \"../utils/useMountedState\";\n\n/**\n * Returns an array of all rows matching the provided filter\n */\nexport function useRows<C extends StoreConfig, T extends keyof C[\"tables\"] & string>(\n storeCache: DatabaseClient<C>,\n filter?: FilterOptions<C, T>\n) {\n const [rows, setRows] = useMountedState<ScanResult<C, T>>([]);\n const filterMemo = useDeepMemo(filter);\n\n useEffect(() => {\n storeCache.scan(filterMemo).then(setRows);\n\n const unsubscribePromise = storeCache.subscribe(() => {\n // very naive implementation for now, but easier and probably more efficient than\n // manually looping through the `rows` array for every update event\n storeCache.scan(filterMemo).then(setRows);\n }, filterMemo);\n\n return () => {\n unsubscribePromise.then((unsubscribe) => unsubscribe());\n };\n }, [filterMemo, setRows, storeCache]);\n\n return rows;\n}\n","import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from \"react\";\n\n/**\n * Use in place of useState when the component may be unmounted before the state is updated.\n */\nexport function useMountedState<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {\n const [state, setState] = useState<T>(initialState);\n const mountedRef = useRef(false);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n });\n const mountedSetState: typeof setState = useCallback((...args) => {\n if (mountedRef.current) {\n setState(...args);\n } else {\n console.warn(\"Ignoring `setState` call because component unmounted\", ...args);\n }\n }, []);\n return [state, mountedSetState];\n}\n","import { StoreConfig } from \"@latticexyz/store\";\nimport { DatabaseClient, Key, ScanResult } from \"@latticexyz/store-cache\";\nimport { useRows } from \"./useRows\";\n\nexport type UseRowFilterOptions<\n C extends StoreConfig = StoreConfig,\n T extends keyof C[\"tables\"] & string = keyof C[\"tables\"] & string\n> = {\n namespace?: C[\"namespace\"];\n table: T;\n key: Key<C, T>;\n};\n\n/**\n * Returns a single row of a given table at the given key, updates when the key changes\n */\nexport function useRow<C extends StoreConfig, T extends keyof C[\"tables\"] & string>(\n storeCache: DatabaseClient<C>,\n filter: UseRowFilterOptions<C, T>\n): ScanResult<C, T>[0] | undefined {\n const { namespace, table, key } = filter;\n return useRows(storeCache, { namespace, table, key: { eq: key } })[0];\n}\n"],"mappings":"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","names":["defineQuery","getComponentValue","Has","isComponentUpdate","useEffect","useState","useComponentValue","component","entity","defaultValue","value","setValue","subscription","update","nextValue","useEffect","useState","useDeprecatedComputedValue","computedValue","value","setValue","unsubscribe","defineQuery","useEffect","useMemo","useState","useEffect","useState","isEqual","useDeepMemo","currentValue","stableValue","setStableValue","isEqual","distinctUntilChanged","map","useEntityQuery","fragments","options","updateOnValueChange","stableFragments","useDeepMemo","query","useMemo","defineQuery","entities","setEntities","useState","useEffect","observable","a","subscription","useEffect","useState","useObservableValue","observable","defaultValue","value","setValue","subscription","useEffect","useCallback","useEffect","useRef","useState","useMountedState","initialState","state","setState","mountedRef","mountedSetState","args","useRows","storeCache","filter","rows","setRows","useMountedState","filterMemo","useDeepMemo","useEffect","unsubscribePromise","unsubscribe","useRow","storeCache","filter","namespace","table","key","useRows"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@latticexyz/react",
|
|
3
|
+
"version": "0.0.0-indexing-20230707130424",
|
|
4
|
+
"description": "React tools for MUD client.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/latticexyz/mud.git",
|
|
8
|
+
"directory": "packages/react"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"types": "src/index.ts",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"fast-deep-equal": "^3.1.3",
|
|
18
|
+
"mobx": "^6.7.0",
|
|
19
|
+
"react": "^18.2.0",
|
|
20
|
+
"rxjs": "7.5.5",
|
|
21
|
+
"@latticexyz/recs": "1.42.0",
|
|
22
|
+
"@latticexyz/store": "0.0.0-indexing-20230707130424",
|
|
23
|
+
"@latticexyz/store-cache": "0.0.0-indexing-20230707130424"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@testing-library/react-hooks": "^8.0.1",
|
|
27
|
+
"@types/react": "^18.2.6",
|
|
28
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
29
|
+
"eslint-plugin-react": "7.31.11",
|
|
30
|
+
"eslint-plugin-react-hooks": "4.6.0",
|
|
31
|
+
"react-test-renderer": "^18.2.0",
|
|
32
|
+
"tsup": "^6.7.0",
|
|
33
|
+
"typedoc": "0.23.21",
|
|
34
|
+
"typedoc-plugin-markdown": "^3.13.6",
|
|
35
|
+
"vite": "^4.3.6",
|
|
36
|
+
"vitest": "0.31.4"
|
|
37
|
+
},
|
|
38
|
+
"gitHead": "914a1e0ae4a573d685841ca2ea921435057deb8f",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "pnpm run build:js",
|
|
41
|
+
"build:js": "tsup",
|
|
42
|
+
"clean": "pnpm run clean:js",
|
|
43
|
+
"clean:js": "rimraf dist",
|
|
44
|
+
"dev": "tsup --watch",
|
|
45
|
+
"docs": "rimraf API && typedoc src && find API -type f -name '*.md' -exec sed -E -i \"\" \"s/(#.*)(<.*>)/\\1/\" {} \\;",
|
|
46
|
+
"test": "tsc --noEmit && vitest --run"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-hooks";
|
|
2
|
+
import { UseRowFilterOptions, useRow } from "./useRow";
|
|
3
|
+
import { mudConfig } from "@latticexyz/store/register";
|
|
4
|
+
import { KeyValue, createDatabase, createDatabaseClient } from "@latticexyz/store-cache";
|
|
5
|
+
import { describe, it, beforeEach, expect } from "vitest";
|
|
6
|
+
|
|
7
|
+
const config = mudConfig({
|
|
8
|
+
tables: {
|
|
9
|
+
MultiKey: { keySchema: { first: "bytes32", second: "uint32" }, schema: "int32" },
|
|
10
|
+
Position: { schema: { x: "int32", y: "int32" } },
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("useRow", () => {
|
|
15
|
+
let db: ReturnType<typeof createDatabase>;
|
|
16
|
+
let client: ReturnType<typeof createDatabaseClient<typeof config>>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
db = createDatabase();
|
|
20
|
+
client = createDatabaseClient(db, config);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return the row of the position table with the specified key", async () => {
|
|
24
|
+
const { result } = renderHook(() => useRow(client, { table: "Position", key: { key: "0x01" } }));
|
|
25
|
+
expect(result.current).toBe(undefined);
|
|
26
|
+
|
|
27
|
+
const positionUpdates: KeyValue<typeof config, "Position">[] = [
|
|
28
|
+
{ key: { key: "0x00" }, value: { x: 1, y: 2 } },
|
|
29
|
+
{ key: { key: "0x01" }, value: { x: 2, y: 3 } },
|
|
30
|
+
{ key: { key: "0x02" }, value: { x: 3, y: 4 } },
|
|
31
|
+
{ key: { key: "0x03" }, value: { x: 4, y: 5 } },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const multiKeyUpdates: KeyValue<typeof config, "MultiKey">[] = [
|
|
35
|
+
{ key: { first: "0x00", second: 4 }, value: { value: 1 } },
|
|
36
|
+
{ key: { first: "0x01", second: 3 }, value: { value: 2 } },
|
|
37
|
+
{ key: { first: "0x02", second: 2 }, value: { value: 3 } },
|
|
38
|
+
{ key: { first: "0x03", second: 1 }, value: { value: 4 } },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
await act(async () => {
|
|
42
|
+
// Set values in the tables
|
|
43
|
+
for (const update of positionUpdates) {
|
|
44
|
+
await client.tables.Position.set(update.key, update.value);
|
|
45
|
+
}
|
|
46
|
+
for (const update of multiKeyUpdates) {
|
|
47
|
+
await client.tables.MultiKey.set(update.key, update.value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.current).toEqual({
|
|
52
|
+
key: { key: "0x01" },
|
|
53
|
+
value: { x: 2, y: 3 },
|
|
54
|
+
namespace: config["namespace"],
|
|
55
|
+
table: "Position",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await act(async () => {
|
|
59
|
+
for (const update of positionUpdates.slice(0, 3)) {
|
|
60
|
+
await client.tables.Position.remove(update.key);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.current).toBe(undefined);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should re-render only when the position value of the specified key changes", async () => {
|
|
68
|
+
const { result } = renderHook(() => useRow(client, { table: "Position", key: { key: "0x00" } }));
|
|
69
|
+
expect(result.all.length).toBe(1);
|
|
70
|
+
|
|
71
|
+
// Update the position table
|
|
72
|
+
await act(async () => {
|
|
73
|
+
await client.tables.Position.set({ key: "0x00" }, { x: 1, y: 2 });
|
|
74
|
+
});
|
|
75
|
+
expect(result.all.length).toBe(3);
|
|
76
|
+
expect(result.current).toEqual({
|
|
77
|
+
key: { key: "0x00" },
|
|
78
|
+
value: { x: 1, y: 2 },
|
|
79
|
+
namespace: config["namespace"],
|
|
80
|
+
table: "Position",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Update an unrelated table
|
|
84
|
+
await act(async () => {
|
|
85
|
+
await client.tables.MultiKey.set({ first: "0x03", second: 1 }, { value: 4 });
|
|
86
|
+
});
|
|
87
|
+
expect(result.all.length).toBe(3);
|
|
88
|
+
expect(result.current).toEqual({
|
|
89
|
+
key: { key: "0x00" },
|
|
90
|
+
value: { x: 1, y: 2 },
|
|
91
|
+
namespace: config["namespace"],
|
|
92
|
+
table: "Position",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Update the position table
|
|
96
|
+
await act(async () => {
|
|
97
|
+
await client.tables.Position.set({ key: "0x00" }, { x: 2, y: 2 });
|
|
98
|
+
});
|
|
99
|
+
expect(result.all.length).toBe(4);
|
|
100
|
+
expect(result.current).toEqual({
|
|
101
|
+
key: { key: "0x00" },
|
|
102
|
+
value: { x: 2, y: 2 },
|
|
103
|
+
namespace: config["namespace"],
|
|
104
|
+
table: "Position",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Update an unrelated key
|
|
108
|
+
await act(async () => {
|
|
109
|
+
await client.tables.Position.set({ key: "0x01" }, { x: 2, y: 2 });
|
|
110
|
+
});
|
|
111
|
+
expect(result.all.length).toBe(4);
|
|
112
|
+
expect(result.current).toEqual({
|
|
113
|
+
key: { key: "0x00" },
|
|
114
|
+
value: { x: 2, y: 2 },
|
|
115
|
+
namespace: config["namespace"],
|
|
116
|
+
table: "Position",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Update the position table
|
|
120
|
+
await act(async () => {
|
|
121
|
+
await client.tables.Position.remove({ key: "0x00" });
|
|
122
|
+
});
|
|
123
|
+
expect(result.all.length).toBe(5);
|
|
124
|
+
expect(result.current).toEqual(undefined);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should re-render when the filter changes", async () => {
|
|
128
|
+
const { result, rerender, waitForNextUpdate } = renderHook(({ filter }) => useRow(client, filter), {
|
|
129
|
+
initialProps: {
|
|
130
|
+
filter: { table: "Position", key: { key: "0x01" } } as UseRowFilterOptions<typeof config>,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.all.length).toBe(1);
|
|
135
|
+
expect(result.current).toBe(undefined);
|
|
136
|
+
|
|
137
|
+
const positionUpdates: KeyValue<typeof config, "Position">[] = [
|
|
138
|
+
{ key: { key: "0x00" }, value: { x: 1, y: 2 } },
|
|
139
|
+
{ key: { key: "0x01" }, value: { x: 2, y: 3 } },
|
|
140
|
+
{ key: { key: "0x02" }, value: { x: 3, y: 4 } },
|
|
141
|
+
{ key: { key: "0x03" }, value: { x: 4, y: 5 } },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const multiKeyUpdates: KeyValue<typeof config, "MultiKey">[] = [
|
|
145
|
+
{ key: { first: "0x00", second: 4 }, value: { value: 1 } },
|
|
146
|
+
{ key: { first: "0x01", second: 3 }, value: { value: 2 } },
|
|
147
|
+
{ key: { first: "0x02", second: 2 }, value: { value: 3 } },
|
|
148
|
+
{ key: { first: "0x03", second: 1 }, value: { value: 4 } },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
await act(async () => {
|
|
152
|
+
// Set values in the tables
|
|
153
|
+
for (const update of positionUpdates) {
|
|
154
|
+
await client.tables.Position.set(update.key, update.value);
|
|
155
|
+
}
|
|
156
|
+
for (const update of multiKeyUpdates) {
|
|
157
|
+
await client.tables.MultiKey.set(update.key, update.value);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result.all.length).toBe(3);
|
|
162
|
+
expect(result.current).toEqual({
|
|
163
|
+
key: { key: "0x01" },
|
|
164
|
+
value: { x: 2, y: 3 },
|
|
165
|
+
namespace: config["namespace"],
|
|
166
|
+
table: "Position",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Change the filter
|
|
170
|
+
rerender({ filter: { table: "Position", key: { key: "0x02" } } });
|
|
171
|
+
await waitForNextUpdate();
|
|
172
|
+
|
|
173
|
+
// Expect hook to rerender three times:
|
|
174
|
+
// 1. New prop, everything else changes the same
|
|
175
|
+
// 2. `filterMemo` is updated by `useDeepMemo` because of the new prop
|
|
176
|
+
// 3. `useEffect` runs because of the new `filterMemo`, scan is executed, new rows are returned
|
|
177
|
+
expect(result.all.length).toBe(6);
|
|
178
|
+
expect(result.current).toEqual({
|
|
179
|
+
key: { key: "0x02" },
|
|
180
|
+
value: { x: 3, y: 4 },
|
|
181
|
+
namespace: config["namespace"],
|
|
182
|
+
table: "Position",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Change the filter
|
|
186
|
+
rerender({ filter: { table: "MultiKey", key: { first: "0x00", second: 4 } } });
|
|
187
|
+
await waitForNextUpdate();
|
|
188
|
+
|
|
189
|
+
expect(result.all.length).toBe(9);
|
|
190
|
+
expect(result.current).toEqual({
|
|
191
|
+
key: { first: "0x00", second: 4 },
|
|
192
|
+
value: { value: 1 },
|
|
193
|
+
namespace: config["namespace"],
|
|
194
|
+
table: "MultiKey",
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { StoreConfig } from "@latticexyz/store";
|
|
2
|
+
import { DatabaseClient, Key, ScanResult } from "@latticexyz/store-cache";
|
|
3
|
+
import { useRows } from "./useRows";
|
|
4
|
+
|
|
5
|
+
export type UseRowFilterOptions<
|
|
6
|
+
C extends StoreConfig = StoreConfig,
|
|
7
|
+
T extends keyof C["tables"] & string = keyof C["tables"] & string
|
|
8
|
+
> = {
|
|
9
|
+
namespace?: C["namespace"];
|
|
10
|
+
table: T;
|
|
11
|
+
key: Key<C, T>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a single row of a given table at the given key, updates when the key changes
|
|
16
|
+
*/
|
|
17
|
+
export function useRow<C extends StoreConfig, T extends keyof C["tables"] & string>(
|
|
18
|
+
storeCache: DatabaseClient<C>,
|
|
19
|
+
filter: UseRowFilterOptions<C, T>
|
|
20
|
+
): ScanResult<C, T>[0] | undefined {
|
|
21
|
+
const { namespace, table, key } = filter;
|
|
22
|
+
return useRows(storeCache, { namespace, table, key: { eq: key } })[0];
|
|
23
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-hooks";
|
|
2
|
+
import { useRows } from "./useRows";
|
|
3
|
+
import { mudConfig } from "@latticexyz/store/register";
|
|
4
|
+
import { KeyValue, createDatabase, createDatabaseClient } from "@latticexyz/store-cache";
|
|
5
|
+
import { describe, it, beforeEach, expect } from "vitest";
|
|
6
|
+
|
|
7
|
+
const config = mudConfig({
|
|
8
|
+
tables: {
|
|
9
|
+
MultiKey: { keySchema: { first: "bytes32", second: "uint32" }, schema: "int32" },
|
|
10
|
+
Position: { schema: { x: "int32", y: "int32" } },
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("useRows", () => {
|
|
15
|
+
let db: ReturnType<typeof createDatabase>;
|
|
16
|
+
let client: ReturnType<typeof createDatabaseClient<typeof config>>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
db = createDatabase();
|
|
20
|
+
client = createDatabaseClient(db, config);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return all rows of the position table", async () => {
|
|
24
|
+
const { result } = renderHook(() => useRows(client, { table: "Position" }));
|
|
25
|
+
expect(result.current.length).toBe(0);
|
|
26
|
+
|
|
27
|
+
const positionUpdates: KeyValue<typeof config, "Position">[] = [
|
|
28
|
+
{ key: { key: "0x00" }, value: { x: 1, y: 2 } },
|
|
29
|
+
{ key: { key: "0x01" }, value: { x: 2, y: 3 } },
|
|
30
|
+
{ key: { key: "0x02" }, value: { x: 3, y: 4 } },
|
|
31
|
+
{ key: { key: "0x03" }, value: { x: 4, y: 5 } },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const multiKeyUpdates: KeyValue<typeof config, "MultiKey">[] = [
|
|
35
|
+
{ key: { first: "0x00", second: 4 }, value: { value: 1 } },
|
|
36
|
+
{ key: { first: "0x01", second: 3 }, value: { value: 2 } },
|
|
37
|
+
{ key: { first: "0x02", second: 2 }, value: { value: 3 } },
|
|
38
|
+
{ key: { first: "0x03", second: 1 }, value: { value: 4 } },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
await act(async () => {
|
|
42
|
+
// Set values in the tables
|
|
43
|
+
for (const update of positionUpdates) {
|
|
44
|
+
await client.tables.Position.set(update.key, update.value);
|
|
45
|
+
}
|
|
46
|
+
for (const update of multiKeyUpdates) {
|
|
47
|
+
await client.tables.MultiKey.set(update.key, update.value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.current.length).toBe(positionUpdates.length);
|
|
52
|
+
expect(result.current).toEqual([
|
|
53
|
+
...positionUpdates.map((row) => ({ ...row, namespace: config["namespace"], table: "Position" })),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
await act(async () => {
|
|
57
|
+
for (const update of positionUpdates.slice(0, 3)) {
|
|
58
|
+
await client.tables.Position.remove(update.key);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.current.length).toBe(1);
|
|
63
|
+
expect(result.current).toEqual([
|
|
64
|
+
{ key: { key: "0x03" }, value: { x: 4, y: 5 }, namespace: config["namespace"], table: "Position" },
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should re-render only when the position table changes", async () => {
|
|
69
|
+
const { result } = renderHook(() => useRows(client, { namespace: config["namespace"], table: "Position" }));
|
|
70
|
+
expect(result.all.length).toBe(1);
|
|
71
|
+
|
|
72
|
+
// Update the position table
|
|
73
|
+
await act(async () => {
|
|
74
|
+
await client.tables.Position.set({ key: "0x00" }, { x: 1, y: 2 });
|
|
75
|
+
});
|
|
76
|
+
expect(result.all.length).toBe(3);
|
|
77
|
+
expect(result.current).toEqual([
|
|
78
|
+
{ key: { key: "0x00" }, value: { x: 1, y: 2 }, namespace: config["namespace"], table: "Position" },
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// Update an unrelated table
|
|
82
|
+
await act(async () => {
|
|
83
|
+
await client.tables.MultiKey.set({ first: "0x03", second: 1 }, { value: 4 });
|
|
84
|
+
});
|
|
85
|
+
expect(result.all.length).toBe(3);
|
|
86
|
+
expect(result.current).toEqual([
|
|
87
|
+
{ key: { key: "0x00" }, value: { x: 1, y: 2 }, namespace: config["namespace"], table: "Position" },
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Update the position table
|
|
91
|
+
await act(async () => {
|
|
92
|
+
await client.tables.Position.set({ key: "0x00" }, { x: 2, y: 2 });
|
|
93
|
+
});
|
|
94
|
+
expect(result.all.length).toBe(4);
|
|
95
|
+
expect(result.current).toEqual([
|
|
96
|
+
{ key: { key: "0x00" }, value: { x: 2, y: 2 }, namespace: config["namespace"], table: "Position" },
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
// Update an unrelated table
|
|
100
|
+
await act(async () => {
|
|
101
|
+
await client.tables.MultiKey.remove({ first: "0x03", second: 1 });
|
|
102
|
+
});
|
|
103
|
+
expect(result.all.length).toBe(4);
|
|
104
|
+
expect(result.current).toEqual([
|
|
105
|
+
{ key: { key: "0x00" }, value: { x: 2, y: 2 }, namespace: config["namespace"], table: "Position" },
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
// Update the position table
|
|
109
|
+
await act(async () => {
|
|
110
|
+
await client.tables.Position.remove({ key: "0x00" });
|
|
111
|
+
});
|
|
112
|
+
expect(result.all.length).toBe(5);
|
|
113
|
+
expect(result.current).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should re-render when the filter changes", async () => {
|
|
117
|
+
const { result, rerender, waitForNextUpdate } = renderHook(({ filter }) => useRows(client, filter), {
|
|
118
|
+
initialProps: { filter: { table: "Position" as keyof (typeof config)["tables"] } },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.all.length).toBe(1);
|
|
122
|
+
expect(result.current.length).toBe(0);
|
|
123
|
+
|
|
124
|
+
const positionUpdates: KeyValue<typeof config, "Position">[] = [
|
|
125
|
+
{ key: { key: "0x00" }, value: { x: 1, y: 2 } },
|
|
126
|
+
{ key: { key: "0x01" }, value: { x: 2, y: 3 } },
|
|
127
|
+
{ key: { key: "0x02" }, value: { x: 3, y: 4 } },
|
|
128
|
+
{ key: { key: "0x03" }, value: { x: 4, y: 5 } },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const multiKeyUpdates: KeyValue<typeof config, "MultiKey">[] = [
|
|
132
|
+
{ key: { first: "0x00", second: 4 }, value: { value: 1 } },
|
|
133
|
+
{ key: { first: "0x01", second: 3 }, value: { value: 2 } },
|
|
134
|
+
{ key: { first: "0x02", second: 2 }, value: { value: 3 } },
|
|
135
|
+
{ key: { first: "0x03", second: 1 }, value: { value: 4 } },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
await act(async () => {
|
|
139
|
+
// Set values in the tables
|
|
140
|
+
for (const update of positionUpdates) {
|
|
141
|
+
await client.tables.Position.set(update.key, update.value);
|
|
142
|
+
}
|
|
143
|
+
for (const update of multiKeyUpdates) {
|
|
144
|
+
await client.tables.MultiKey.set(update.key, update.value);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.all.length).toBe(6);
|
|
149
|
+
expect(result.current.length).toBe(positionUpdates.length);
|
|
150
|
+
expect(result.current).toEqual([
|
|
151
|
+
...positionUpdates.map((row) => ({ ...row, namespace: config["namespace"], table: "Position" })),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// Change the filter
|
|
155
|
+
rerender({ filter: { table: "MultiKey" } });
|
|
156
|
+
await waitForNextUpdate();
|
|
157
|
+
|
|
158
|
+
// Expect hook to rerender three times:
|
|
159
|
+
// 1. New prop, everything else changes the same
|
|
160
|
+
// 2. `filterMemo` is updated by `useDeepMemo` because of the new prop
|
|
161
|
+
// 3. `useEffect` runs because of the new `filterMemo`, scan is executed, new rows are returned
|
|
162
|
+
expect(result.all.length).toBe(9);
|
|
163
|
+
expect(result.current.length).toBe(multiKeyUpdates.length);
|
|
164
|
+
expect(result.current).toEqual([
|
|
165
|
+
...multiKeyUpdates.map((row) => ({ ...row, namespace: config["namespace"], table: "MultiKey" })),
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DatabaseClient, FilterOptions, ScanResult } from "@latticexyz/store-cache";
|
|
2
|
+
import { StoreConfig } from "@latticexyz/store";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useDeepMemo } from "../utils/useDeepMemo";
|
|
5
|
+
import { useMountedState } from "../utils/useMountedState";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns an array of all rows matching the provided filter
|
|
9
|
+
*/
|
|
10
|
+
export function useRows<C extends StoreConfig, T extends keyof C["tables"] & string>(
|
|
11
|
+
storeCache: DatabaseClient<C>,
|
|
12
|
+
filter?: FilterOptions<C, T>
|
|
13
|
+
) {
|
|
14
|
+
const [rows, setRows] = useMountedState<ScanResult<C, T>>([]);
|
|
15
|
+
const filterMemo = useDeepMemo(filter);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
storeCache.scan(filterMemo).then(setRows);
|
|
19
|
+
|
|
20
|
+
const unsubscribePromise = storeCache.subscribe(() => {
|
|
21
|
+
// very naive implementation for now, but easier and probably more efficient than
|
|
22
|
+
// manually looping through the `rows` array for every update event
|
|
23
|
+
storeCache.scan(filterMemo).then(setRows);
|
|
24
|
+
}, filterMemo);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
unsubscribePromise.then((unsubscribe) => unsubscribe());
|
|
28
|
+
};
|
|
29
|
+
}, [filterMemo, setRows, storeCache]);
|
|
30
|
+
|
|
31
|
+
return rows;
|
|
32
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
15
|
+
|
|
16
|
+
describe("useComponentValue", () => {
|
|
17
|
+
let world: World;
|
|
18
|
+
let Position: Component<{
|
|
19
|
+
x: Type.Number;
|
|
20
|
+
y: Type.Number;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
world = createWorld();
|
|
25
|
+
Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return Position value for entity", () => {
|
|
29
|
+
const entity = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
30
|
+
|
|
31
|
+
const { result } = renderHook(() => useComponentValue(Position, entity));
|
|
32
|
+
expect(result.current).toEqual({ x: 1, y: 1 });
|
|
33
|
+
|
|
34
|
+
act(() => {
|
|
35
|
+
setComponent(Position, entity, { x: 0, y: 0 });
|
|
36
|
+
});
|
|
37
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
removeComponent(Position, entity);
|
|
41
|
+
});
|
|
42
|
+
expect(result.current).toBe(undefined);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should re-render only when Position changes for entity", () => {
|
|
46
|
+
const entity = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
47
|
+
const otherEntity = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
48
|
+
|
|
49
|
+
const { result } = renderHook(() => useComponentValue(Position, entity));
|
|
50
|
+
expect(result.all.length).toBe(2);
|
|
51
|
+
expect(result.current).toEqual({ x: 1, y: 1 });
|
|
52
|
+
|
|
53
|
+
act(() => {
|
|
54
|
+
setComponent(Position, entity, { x: 0, y: 0 });
|
|
55
|
+
});
|
|
56
|
+
expect(result.all.length).toBe(3);
|
|
57
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
58
|
+
|
|
59
|
+
act(() => {
|
|
60
|
+
setComponent(Position, otherEntity, { x: 0, y: 0 });
|
|
61
|
+
removeComponent(Position, otherEntity);
|
|
62
|
+
});
|
|
63
|
+
expect(result.all.length).toBe(3);
|
|
64
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
65
|
+
|
|
66
|
+
act(() => {
|
|
67
|
+
removeComponent(Position, entity);
|
|
68
|
+
});
|
|
69
|
+
expect(result.all.length).toBe(4);
|
|
70
|
+
expect(result.current).toBe(undefined);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should return default value when Position is not set", () => {
|
|
74
|
+
const entity = createEntity(world);
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useComponentValue(Position, entity, { x: -1, y: -1 }));
|
|
77
|
+
expect(result.current).toEqual({ x: -1, y: -1 });
|
|
78
|
+
|
|
79
|
+
act(() => {
|
|
80
|
+
setComponent(Position, entity, { x: 0, y: 0 });
|
|
81
|
+
});
|
|
82
|
+
expect(result.current).toEqual({ x: 0, y: 0 });
|
|
83
|
+
|
|
84
|
+
act(() => {
|
|
85
|
+
removeComponent(Position, entity);
|
|
86
|
+
});
|
|
87
|
+
expect(result.current).toEqual({ x: -1, y: -1 });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -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,164 @@
|
|
|
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
|
+
import { describe, beforeEach, it, expect } from "vitest";
|
|
17
|
+
|
|
18
|
+
describe("useEntityQuery", () => {
|
|
19
|
+
let world: World;
|
|
20
|
+
let Position: Component<{
|
|
21
|
+
x: Type.Number;
|
|
22
|
+
y: Type.Number;
|
|
23
|
+
}>;
|
|
24
|
+
let OwnedBy: Component<{ value: Type.Entity }>;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
world = createWorld();
|
|
28
|
+
Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" });
|
|
29
|
+
OwnedBy = defineComponent(world, { value: Type.Entity }, { id: "OwnedBy" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should find entities with Position component", () => {
|
|
33
|
+
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
34
|
+
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
35
|
+
const entity3 = createEntity(world, []);
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(() => useEntityQuery([Has(Position)], { updateOnValueChange: false }));
|
|
38
|
+
const { result: resultOnValueChange } = renderHook(() =>
|
|
39
|
+
useEntityQuery([Has(Position)], { updateOnValueChange: true })
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result.current.length).toBe(2);
|
|
43
|
+
expect(result.current).toContain(entity1);
|
|
44
|
+
expect(result.current).toContain(entity2);
|
|
45
|
+
expect(result.current).not.toContain(entity3);
|
|
46
|
+
expect(resultOnValueChange.current).toEqual(result.current);
|
|
47
|
+
|
|
48
|
+
act(() => {
|
|
49
|
+
setComponent(Position, entity3, { x: 0, y: 0 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result.current.length).toBe(3);
|
|
53
|
+
expect(result.current).toContain(entity1);
|
|
54
|
+
expect(result.current).toContain(entity2);
|
|
55
|
+
expect(result.current).toContain(entity3);
|
|
56
|
+
expect(resultOnValueChange.current).toEqual(result.current);
|
|
57
|
+
|
|
58
|
+
act(() => {
|
|
59
|
+
removeComponent(Position, entity1);
|
|
60
|
+
removeComponent(Position, entity3);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.current.length).toBe(1);
|
|
64
|
+
expect(result.current).not.toContain(entity1);
|
|
65
|
+
expect(result.current).toContain(entity2);
|
|
66
|
+
expect(result.current).not.toContain(entity3);
|
|
67
|
+
expect(resultOnValueChange.current).toEqual(result.current);
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
removeComponent(Position, entity2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.length).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should re-render only when Position changes", () => {
|
|
77
|
+
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
78
|
+
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
79
|
+
const entity3 = createEntity(world, []);
|
|
80
|
+
|
|
81
|
+
const { result } = renderHook(() => useEntityQuery([Has(Position)], { updateOnValueChange: false }));
|
|
82
|
+
const { result: resultOnValueChange } = renderHook(() =>
|
|
83
|
+
useEntityQuery([Has(Position)], { updateOnValueChange: true })
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(result.all).toHaveLength(2);
|
|
87
|
+
expect(result.current).toHaveLength(2);
|
|
88
|
+
expect(result.current).toContain(entity1);
|
|
89
|
+
expect(result.current).toContain(entity2);
|
|
90
|
+
expect(result.current).not.toContain(entity3);
|
|
91
|
+
|
|
92
|
+
// Changing an entity's component value should NOT re-render,
|
|
93
|
+
// unless updateOnValueChange === true
|
|
94
|
+
act(() => {
|
|
95
|
+
setComponent(Position, entity2, { x: 0, y: 0 });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.all).toHaveLength(2);
|
|
99
|
+
expect(resultOnValueChange.all).toHaveLength(3);
|
|
100
|
+
|
|
101
|
+
// Changing a different component value should NOT re-render
|
|
102
|
+
act(() => {
|
|
103
|
+
setComponent(OwnedBy, entity2, { value: entity1 });
|
|
104
|
+
setComponent(OwnedBy, entity3, { value: entity1 });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.all).toHaveLength(2);
|
|
108
|
+
expect(resultOnValueChange.all).toHaveLength(3);
|
|
109
|
+
|
|
110
|
+
// Changing which entities have the component should re-render
|
|
111
|
+
act(() => {
|
|
112
|
+
setComponent(Position, entity3, { x: 0, y: 0 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(result.all).toHaveLength(3);
|
|
116
|
+
expect(resultOnValueChange.all).toHaveLength(4);
|
|
117
|
+
expect(result.current).toHaveLength(3);
|
|
118
|
+
expect(result.current).toContain(entity1);
|
|
119
|
+
expect(result.current).toContain(entity2);
|
|
120
|
+
expect(result.current).toContain(entity3);
|
|
121
|
+
|
|
122
|
+
// Changing which entities have the component should re-render
|
|
123
|
+
act(() => {
|
|
124
|
+
removeComponent(Position, entity1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(result.all).toHaveLength(4);
|
|
128
|
+
expect(resultOnValueChange.all).toHaveLength(5);
|
|
129
|
+
expect(result.current).toHaveLength(2);
|
|
130
|
+
expect(result.current).toContain(entity2);
|
|
131
|
+
expect(result.current).toContain(entity3);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should re-render as hook arguments change", () => {
|
|
135
|
+
// TODO: reduce re-renders during argument changes?
|
|
136
|
+
|
|
137
|
+
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]);
|
|
138
|
+
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
139
|
+
const entity3 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]);
|
|
140
|
+
|
|
141
|
+
const { result, rerender } = renderHook(({ x, y }) => useEntityQuery([HasValue(Position, { x, y })]), {
|
|
142
|
+
initialProps: { x: 1, y: 1 },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.all).toHaveLength(2);
|
|
146
|
+
expect(result.current).toHaveLength(1);
|
|
147
|
+
expect(result.current).toContain(entity1);
|
|
148
|
+
|
|
149
|
+
rerender({ x: 1, y: 1 });
|
|
150
|
+
expect(result.all).toHaveLength(3);
|
|
151
|
+
expect(result.current).toHaveLength(1);
|
|
152
|
+
expect(result.current).toContain(entity1);
|
|
153
|
+
|
|
154
|
+
rerender({ x: 2, y: 2 });
|
|
155
|
+
expect(result.all).toHaveLength(6);
|
|
156
|
+
expect(result.current).toHaveLength(2);
|
|
157
|
+
expect(result.current).toContain(entity2);
|
|
158
|
+
expect(result.current).toContain(entity3);
|
|
159
|
+
|
|
160
|
+
rerender({ x: -1, y: -1 });
|
|
161
|
+
expect(result.all).toHaveLength(9);
|
|
162
|
+
expect(result.current).toHaveLength(0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Use in place of useState when the component may be unmounted before the state is updated.
|
|
5
|
+
*/
|
|
6
|
+
export function useMountedState<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
|
|
7
|
+
const [state, setState] = useState<T>(initialState);
|
|
8
|
+
const mountedRef = useRef(false);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
mountedRef.current = true;
|
|
11
|
+
return () => {
|
|
12
|
+
mountedRef.current = false;
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
const mountedSetState: typeof setState = useCallback((...args) => {
|
|
16
|
+
if (mountedRef.current) {
|
|
17
|
+
setState(...args);
|
|
18
|
+
} else {
|
|
19
|
+
console.warn("Ignoring `setState` call because component unmounted", ...args);
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
return [state, mountedSetState];
|
|
23
|
+
}
|