forked from latticexyz/mud
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react): add react package (latticexyz#294)
* feat: add mud-react * feat: rename mud-react to react * chore(react): clean up * chore(react): rename mobx-dependent module * feat(react): add useObservableValue hook * chore(react): add deprecation notice * chore(react): remove unused deps * fix(react): version * fix(react): use @deprecated tag Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com> * chore(react): more deprecated notices * test(react): set up test framework, add tests for useEntityQuery * test(react): add tests for useComponentValue * test(react): refactor useComponentValue to only re-render when entity's value changes * docs(react): add README * fix(react): use typed helper for checking if it's a component update Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com> * chore(react): add frolic to codeowners * refactor(react): change useComponentValue args ordering * style(react): simplify tsconfig * fix(react): remove unused dep Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com>
- Loading branch information
Showing
21 changed files
with
1,573 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
# These owners will be the default owners for everything in | ||
# the repo. Unless a later match takes precedence. | ||
|
||
* @alvrs | ||
* @alvrs | ||
|
||
# Package-specific owners. Order is important; the last matching | ||
# pattern takes the most precedence. When someone opens a pull request | ||
# that only modifies a specific package for which there is an owner | ||
# specified, only that owner and not the global owner(s) will be requested | ||
# for a review. | ||
|
||
/packages/services/ @authcall | ||
/packages/services @authcall | ||
/packages/react @holic |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": ["../../.eslintrc", "plugin:react/recommended", "plugin:react-hooks/recommended"], | ||
"plugins": ["react", "react-hooks"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
dist | ||
docs | ||
API |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
* | ||
|
||
!dist/** | ||
!package.json | ||
!README.md | ||
!CHANGELOG.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# MUD React | ||
|
||
React hooks (and more) for building MUD clients. | ||
|
||
### useComponentValue | ||
|
||
Returns the value of the component for the entity, and triggers a re-render as the component is added/removed/updated. | ||
|
||
``` | ||
const position = useComponentValue(entity, Position); | ||
``` | ||
|
||
### useEntityQuery | ||
|
||
Returns all matching `EntityIndex`es for a given entity query, and triggers a re-render as new query results come in. | ||
|
||
``` | ||
const entities = useEntityQuery([Has(Position)]); | ||
const playersAtPosition = useEntityQuery([Has(Player), HasValue(Position, { x: 0, y: 0 })]); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
label: react | ||
order: 8 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ | ||
module.exports = { | ||
preset: "ts-jest", | ||
testEnvironment: "node", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
{ | ||
"name": "@latticexyz/react", | ||
"version": "1.32.0", | ||
"description": "React tools for MUD client.", | ||
"license": "MIT", | ||
"source": "src/index.ts", | ||
"main": "src/index.ts", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/latticexyz/mud.git", | ||
"directory": "packages/react" | ||
}, | ||
"scripts": { | ||
"prepare": "yarn build", | ||
"link": "yarn link", | ||
"docs": "rimraf API && typedoc src && find API -type f -name '*.md' -exec sed -E -i \"\" \"s/(#.*)(<.*>)/\\1/\" {} \\; && echo 'label: API' > API/index.yml", | ||
"test": "tsc && jest", | ||
"prepack": "mv package.json package.json.bak && jq \".main = \\\"dist/index.js\\\"\" package.json.bak > package.json ", | ||
"postpack": "mv package.json.bak package.json || echo 'no package.json.bak'", | ||
"build": "rimraf dist && rollup -c rollup.config.js", | ||
"release": "npm publish || echo 'version already published'" | ||
}, | ||
"devDependencies": { | ||
"@latticexyz/recs": "^1.32.0", | ||
"@rollup/plugin-json": "^4.1.0", | ||
"@rollup/plugin-node-resolve": "^13.1.3", | ||
"@rollup/plugin-typescript": "^8.3.1", | ||
"@testing-library/react-hooks": "^8.0.1", | ||
"@types/react": "^18.0.12", | ||
"eslint-plugin-react": "^7.31.11", | ||
"eslint-plugin-react-hooks": "^4.6.0", | ||
"jest": "^29.3.1", | ||
"mobx": "^6.4.2", | ||
"react": "^18.2.0", | ||
"react-test-renderer": "^18.2.0", | ||
"rimraf": "^3.0.2", | ||
"rollup": "^2.69.0", | ||
"rollup-plugin-commonjs": "^10.1.0", | ||
"rollup-plugin-peer-deps-external": "^2.2.4", | ||
"rxjs": "^7.5.5", | ||
"ts-jest": "^29.0.3", | ||
"tslib": "^2.3.1", | ||
"typedoc": "0.23.21", | ||
"typedoc-plugin-markdown": "^3.13.6", | ||
"typescript": "^4.5.5" | ||
}, | ||
"peerDependencies": { | ||
"@latticexyz/recs": "^1.32.0", | ||
"mobx": "^6.4.2", | ||
"react": "^18.2.0", | ||
"rxjs": "^7.5.5" | ||
}, | ||
"gitHead": "218f56893d268b0c5157a3e4c603b859e287a343" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import typescript from "@rollup/plugin-typescript"; | ||
import { nodeResolve } from "@rollup/plugin-node-resolve"; | ||
import commonjs from "rollup-plugin-commonjs"; | ||
import peerDepsExternal from "rollup-plugin-peer-deps-external"; | ||
import pluginJson from "@rollup/plugin-json" | ||
|
||
import { defineConfig } from "rollup"; | ||
|
||
export default defineConfig({ | ||
input: "./src/index.ts", | ||
treeshake: true, | ||
output: { | ||
dir: "dist", | ||
sourcemap: true, | ||
}, | ||
plugins: [nodeResolve(), typescript(), commonjs(), peerDepsExternal(), pluginJson()], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from "./useComponentValue"; | ||
export * from "./useDeprecatedComputedValue"; | ||
export * from "./useEntityQuery"; | ||
export * from "./useObservableValue"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { renderHook, act } from "@testing-library/react-hooks"; | ||
import { | ||
World, | ||
Type, | ||
createWorld, | ||
defineComponent, | ||
Component, | ||
createEntity, | ||
withValue, | ||
setComponent, | ||
removeComponent, | ||
} from "@latticexyz/recs"; | ||
import { useComponentValue } from "./useComponentValue"; | ||
|
||
describe("useComponentValue", () => { | ||
let world: World; | ||
let Position: Component<{ | ||
x: Type.Number; | ||
y: Type.Number; | ||
}>; | ||
|
||
beforeEach(() => { | ||
world = createWorld(); | ||
Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" }); | ||
}); | ||
|
||
it("should return Position value for entity", () => { | ||
const entity = createEntity(world, [withValue(Position, { x: 1, y: 1 })]); | ||
|
||
const { result } = renderHook(() => useComponentValue(Position, entity)); | ||
expect(result.current).toEqual({ x: 1, y: 1 }); | ||
|
||
act(() => { | ||
setComponent(Position, entity, { x: 0, y: 0 }); | ||
}); | ||
expect(result.current).toEqual({ x: 0, y: 0 }); | ||
|
||
act(() => { | ||
removeComponent(Position, entity); | ||
}); | ||
expect(result.current).toBe(undefined); | ||
}); | ||
|
||
it("should re-render only when Position changes for entity", () => { | ||
const entity = createEntity(world, [withValue(Position, { x: 1, y: 1 })]); | ||
const otherEntity = createEntity(world, [withValue(Position, { x: 2, y: 2 })]); | ||
|
||
const { result } = renderHook(() => useComponentValue(Position, entity)); | ||
expect(result.all.length).toBe(2); | ||
expect(result.current).toEqual({ x: 1, y: 1 }); | ||
|
||
act(() => { | ||
setComponent(Position, entity, { x: 0, y: 0 }); | ||
}); | ||
expect(result.all.length).toBe(3); | ||
expect(result.current).toEqual({ x: 0, y: 0 }); | ||
|
||
act(() => { | ||
setComponent(Position, otherEntity, { x: 0, y: 0 }); | ||
removeComponent(Position, otherEntity); | ||
}); | ||
expect(result.all.length).toBe(3); | ||
expect(result.current).toEqual({ x: 0, y: 0 }); | ||
|
||
act(() => { | ||
removeComponent(Position, entity); | ||
}); | ||
expect(result.all.length).toBe(4); | ||
expect(result.current).toBe(undefined); | ||
}); | ||
|
||
it("should return default value when Position is not set", () => { | ||
const entity = createEntity(world); | ||
|
||
const { result } = renderHook(() => useComponentValue(Position, entity, { x: -1, y: -1 })); | ||
expect(result.current).toEqual({ x: -1, y: -1 }); | ||
|
||
act(() => { | ||
setComponent(Position, entity, { x: 0, y: 0 }); | ||
}); | ||
expect(result.current).toEqual({ x: 0, y: 0 }); | ||
|
||
act(() => { | ||
removeComponent(Position, entity); | ||
}); | ||
expect(result.current).toEqual({ x: -1, y: -1 }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { | ||
Component, | ||
ComponentValue, | ||
defineQuery, | ||
EntityIndex, | ||
getComponentValue, | ||
Has, | ||
isComponentUpdate, | ||
Metadata, | ||
Schema, | ||
} from "@latticexyz/recs"; | ||
import { useEffect, useState } from "react"; | ||
|
||
export function useComponentValue<S extends Schema>( | ||
component: Component<S, Metadata, undefined>, | ||
entityIndex: EntityIndex | undefined, | ||
defaultValue: ComponentValue<S> | ||
): ComponentValue<S>; | ||
|
||
export function useComponentValue<S extends Schema>( | ||
component: Component<S, Metadata, undefined>, | ||
entityIndex: EntityIndex | undefined | ||
): ComponentValue<S> | undefined; | ||
|
||
export function useComponentValue<S extends Schema>( | ||
component: Component<S, Metadata, undefined>, | ||
entityIndex: EntityIndex | undefined, | ||
defaultValue?: ComponentValue<S> | ||
) { | ||
const [value, setValue] = useState(entityIndex != null ? getComponentValue(component, entityIndex) : undefined); | ||
|
||
useEffect(() => { | ||
// component or entityIndex changed, update state to latest value | ||
setValue(entityIndex != null ? getComponentValue(component, entityIndex) : undefined); | ||
if (entityIndex == null) return; | ||
|
||
const queryResult = defineQuery([Has(component)], { runOnInit: false }); | ||
const subscription = queryResult.update$.subscribe((update) => { | ||
if (isComponentUpdate(update, component) && update.entity === entityIndex) { | ||
const [nextValue] = update.value; | ||
setValue(nextValue); | ||
} | ||
}); | ||
return () => subscription.unsubscribe(); | ||
}, [component, entityIndex]); | ||
|
||
return value ?? defaultValue; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { IComputedValue } from "mobx"; | ||
import { useEffect, useState } from "react"; | ||
|
||
/** @deprecated See https://github.com/latticexyz/mud/issues/339 */ | ||
export const useDeprecatedComputedValue = <T>(computedValue: IComputedValue<T>) => { | ||
const [value, setValue] = useState<T>(computedValue.get()); | ||
|
||
useEffect(() => { | ||
const unsubscribe = computedValue.observe_(() => setValue(computedValue.get())); | ||
return () => unsubscribe(); | ||
}, [computedValue]); | ||
|
||
return value; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { renderHook, act } from "@testing-library/react-hooks"; | ||
import { | ||
World, | ||
Type, | ||
createWorld, | ||
defineComponent, | ||
Component, | ||
createEntity, | ||
withValue, | ||
Has, | ||
setComponent, | ||
} from "@latticexyz/recs"; | ||
import { useEntityQuery } from "./useEntityQuery"; | ||
import { useMemo } from "react"; | ||
|
||
describe("useEntityQuery", () => { | ||
let world: World; | ||
let Position: Component<{ | ||
x: Type.Number; | ||
y: Type.Number; | ||
}>; | ||
let OwnedBy: Component<{ value: Type.Entity }>; | ||
|
||
beforeEach(() => { | ||
world = createWorld(); | ||
Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" }); | ||
OwnedBy = defineComponent(world, { value: Type.Entity }, { id: "OwnedBy" }); | ||
}); | ||
|
||
it("should find entities with Position component", () => { | ||
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]); | ||
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]); | ||
const entity3 = createEntity(world, []); | ||
|
||
const { result } = renderHook(() => useEntityQuery(useMemo(() => [Has(Position)], []))); | ||
|
||
expect(result.current.length).toBe(2); | ||
expect(result.current).toContain(entity1); | ||
expect(result.current).toContain(entity2); | ||
expect(result.current).not.toContain(entity3); | ||
}); | ||
|
||
it("should re-render only when Position changes", () => { | ||
const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 1 })]); | ||
const entity2 = createEntity(world, [withValue(Position, { x: 2, y: 2 })]); | ||
const entity3 = createEntity(world, []); | ||
|
||
const { result } = renderHook(() => useEntityQuery(useMemo(() => [Has(Position)], []))); | ||
|
||
expect(result.all.length).toBe(2); | ||
|
||
act(() => { | ||
setComponent(Position, entity2, { x: 0, y: 0 }); | ||
}); | ||
|
||
expect(result.all.length).toBe(3); | ||
|
||
act(() => { | ||
setComponent(OwnedBy, entity2, { value: world.entities[entity1] }); | ||
setComponent(OwnedBy, entity3, { value: world.entities[entity1] }); | ||
}); | ||
|
||
expect(result.all.length).toBe(3); | ||
|
||
act(() => { | ||
setComponent(Position, entity3, { x: 0, y: 0 }); | ||
}); | ||
|
||
expect(result.all.length).toBe(4); | ||
}); | ||
}); |
Oops, something went wrong.