Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stash): add useStash and improve other helpers #3320

Merged
merged 8 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/twenty-boats-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@latticexyz/stash": patch
---

Added `useStash` React hook. It's heavily inspired by Zustand's `useStore` and accepts a stash, a state selector, an an optional equality function to avoid unnecessary re-render cycles when returning unstable values.

Also updated `getRecord` and `getRecords` to each take either a `stash` or `state` object for more ergonomic use with `useStash`.

```ts
import { useStash } from "@latticexyz/stash/react";
import { getRecord } from "@latticexyz/stash";
import config from "../mud.config";

const tables = config.namespaces.app.tables;

export function PlayerName({ playerId }) {
const record = useStash(stash, (state) => getRecord({ state, table: tables.Player, key: { playerId } }));
...
}
```

```ts
import isEqual from "fast-deep-equal";
import { useStash } from "@latticexyz/stash/react";
import { getRecords } from "@latticexyz/stash";
import config from "../mud.config";

export function PlayerNames() {
const record = useStash(stash, (state) => getRecords({ state, table: tables.Player }), { isEqual });
...
}
```
2 changes: 2 additions & 0 deletions packages/common/tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"noErrorTruncation": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
// TODO: enable this
// "noUncheckedIndexedAccess": true,
"sourceMap": true
}
}
7 changes: 7 additions & 0 deletions packages/stash/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": ["../../.eslintrc", "plugin:react/recommended", "plugin:react-hooks/recommended"],
"plugins": ["react", "react-hooks"],
"rules": {
"react/react-in-jsx-scope": "off"
}
}
18 changes: 14 additions & 4 deletions packages/stash/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,36 @@
"build": "tsup",
"clean": "shx rm -rf dist",
"dev": "tsup --watch",
"test": "vitest typecheck --run --passWithNoTests && vitest --run --passWithNoTests",
"test": "tsc -p tsconfig.test.json --noEmit && vitest --run",
"test:ci": "pnpm run test"
},
"dependencies": {
"@ark/util": "catalog:",
"@latticexyz/common": "workspace:*",
"@latticexyz/config": "workspace:*",
"@latticexyz/protocol-parser": "workspace:*",
"@latticexyz/schema-type": "workspace:*",
"@latticexyz/store": "workspace:*",
"react": "^18.2.0",
"viem": "catalog:"
},
"devDependencies": {
"@testing-library/react": "^16.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/react": "18.2.22",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"fast-deep-equal": "^3.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tsup": "^6.7.0",
"vitest": "0.34.6"
"tsup": "^6.7.0"
},
"peerDependencies": {
"react": "18.x"
},
"publishConfig": {
"access": "public"
},
"optionalPeerDependencies": {
"react": "18.x"
}
}
10 changes: 5 additions & 5 deletions packages/stash/src/actions/decodeKey.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Table } from "@latticexyz/config";
import { Key, Stash } from "../common";
import { getKey } from "@latticexyz/protocol-parser/internal";

export type DecodeKeyArgs<table extends Table = Table> = {
stash: Stash;
Expand All @@ -14,9 +15,8 @@ export function decodeKey<table extends Table>({
table,
encodedKey,
}: DecodeKeyArgs<table>): DecodeKeyResult<table> {
const { namespaceLabel, label, key } = table;
const record = stash.get().records[namespaceLabel][label][encodedKey];

// Typecast needed because record values could be arrays, but we know they are not if they are key fields
return Object.fromEntries(Object.entries(record).filter(([field]) => key.includes(field))) as never;
const { namespaceLabel, label } = table;
const record = stash.get().records[namespaceLabel]?.[label]?.[encodedKey];
if (!record) throw new Error(`No record found for key "${encodedKey}".`);
return getKey(table, record);
}
6 changes: 3 additions & 3 deletions packages/stash/src/actions/deleteRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ export function deleteRecord<table extends Table>({ stash, table, key }: DeleteR
}

const encodedKey = encodeKey({ table, key });
const prevRecord = stash.get().records[namespaceLabel][label][encodedKey];
const prevRecord = stash.get().records[namespaceLabel]?.[label]?.[encodedKey];

// Early return if this record doesn't exist
if (prevRecord == null) return;

// Delete record
delete stash._.state.records[namespaceLabel][label][encodedKey];
delete stash._.state.records[namespaceLabel]?.[label]?.[encodedKey];

// Notify table subscribers
const updates = { [encodedKey]: { prev: prevRecord && { ...prevRecord }, current: undefined } };
stash._.tableSubscribers[namespaceLabel][label].forEach((subscriber) => subscriber(updates));
stash._.tableSubscribers[namespaceLabel]?.[label]?.forEach((subscriber) => subscriber(updates));

// Notify stash subscribers
const storeUpdate = { config: {}, records: { [namespaceLabel]: { [label]: updates } } };
Expand Down
7 changes: 3 additions & 4 deletions packages/stash/src/actions/getKeys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Table } from "@latticexyz/config";
import { getKey } from "@latticexyz/protocol-parser/internal";
import { Stash, Keys } from "../common";
import { decodeKey } from "./decodeKey";

export type GetKeysArgs<table extends Table = Table> = {
stash: Stash;
Expand All @@ -11,11 +11,10 @@ export type GetKeysResult<table extends Table = Table> = Keys<table>;

export function getKeys<table extends Table>({ stash, table }: GetKeysArgs<table>): GetKeysResult<table> {
const { namespaceLabel, label } = table;

return Object.fromEntries(
Object.keys(stash.get().records[namespaceLabel][label]).map((encodedKey) => [
Object.entries(stash.get().records[namespaceLabel]?.[label] ?? {}).map(([encodedKey, record]) => [
encodedKey,
decodeKey({ stash, table, encodedKey }),
getKey(table, record),
]),
);
}
Loading
Loading