Skip to content

Commit

Permalink
feat(stash): add useSelector react hook
Browse files Browse the repository at this point in the history
  • Loading branch information
alvrs committed Aug 15, 2024
1 parent 27bd29e commit e51cf7a
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/stash/src/exports/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "../createStash";
export * from "../common";
export * from "../queryFragments";
export * from "../actions";
export * from "../react";
1 change: 1 addition & 0 deletions packages/stash/src/react/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useSelector";
55 changes: 55 additions & 0 deletions packages/stash/src/react/useSelector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from "vitest";
import { renderHook, act } from "@testing-library/react-hooks";
import { useSelector } from "./useSelector";
import { defineStore } from "@latticexyz/store/config/v2";
import { createStash } from "../createStash";

describe("useCustomHook", () => {
it("checks the re-render behavior of the hook", async () => {
const config = defineStore({
namespaces: {
game: {
tables: {
Position: {
schema: { player: "address", x: "uint32", y: "uint32" },
key: ["player"],
},
},
},
},
});
const Position = config.namespaces.game.tables.Position;
const stash = createStash(config);
const player = "0x00";

const { result } = renderHook(() => useSelector(stash, (state) => state.records["game"]["Position"][player]));
expect(result.current).toBe(undefined);

act(() => {
stash.setRecord({ table: Position, key: { player }, record: { x: 1, y: 2 } });
});

// Expect update to have triggered rerender
expect(result.all.length).toBe(2);
expect(result.all).toStrictEqual([undefined, { player, x: 1, y: 2 }]);
expect(result.current).toStrictEqual({ player, x: 1, y: 2 });

act(() => {
stash.setRecord({ table: Position, key: { player: "0x01" }, record: { x: 1, y: 2 } });
});

// Expect unrelated update to not have triggered rerender
expect(result.all.length).toBe(2);
expect(result.all).toStrictEqual([undefined, { player, x: 1, y: 2 }]);
expect(result.current).toStrictEqual({ player, x: 1, y: 2 });

// Expect update to have triggered rerender
act(() => {
stash.setRecord({ table: Position, key: { player }, record: { x: 1, y: 3 } });
});

expect(result.all.length).toBe(3);
expect(result.all).toStrictEqual([undefined, { player, x: 1, y: 2 }, { player, x: 1, y: 3 }]);
expect(result.current).toStrictEqual({ player, x: 1, y: 3 });
});
});
28 changes: 28 additions & 0 deletions packages/stash/src/react/useSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useRef, useState } from "react";
import { State, Stash, StoreConfig } from "../common";
import { subscribeStore } from "../actions";

export function useSelector<config extends StoreConfig, T>(
stash: Stash<config>,
selector: (stash: State<config>) => T,
equals: (a: T, b: T) => boolean = (a, b) => a === b,
): T {
const state = useRef(selector(stash.get()));
const [, forceUpdate] = useState({});

useEffect(() => {
const unsubscribe = subscribeStore({
stash,
subscriber: () => {
const nextState = selector(stash.get());
if (!equals(state.current, nextState)) {
state.current = nextState;
forceUpdate({});
}
},
});
return unsubscribe;
}, []);

return state.current;
}

0 comments on commit e51cf7a

Please sign in to comment.