diff --git a/.changeset/witty-games-raise.md b/.changeset/witty-games-raise.md
new file mode 100644
index 0000000..ec71008
--- /dev/null
+++ b/.changeset/witty-games-raise.md
@@ -0,0 +1,33 @@
+---
+"@udecode/zustood": minor
+---
+
+`react-tracked` support
+
+Use the tracked hooks in React components, no providers needed. Select your
+state and the component will trigger re-renders only if the **accessed property** is changed. Use the `useTracked` method:
+
+```tsx
+// Global tracked hook selectors
+export const useTrackedStore = () => mapValuesKey('useTracked', rootStore);
+
+// with useTrackStore UserEmail Component will only re-render when accessed property owner.email changed
+const UserEmail = () => {
+ const owner = useTrackedStore().repo.owner()
+ return (
+
+ User Email: {owner.email}
+
+ );
+};
+// with useStore UserEmail Component re-render when owner changed, but you can pass equalityFn to avoid it.
+const UserEmail = () => {
+ const owner = useStore().repo.owner()
+ // const owner = useStore().repo.owner((prev, next) => prev.owner.email === next.owner.email)
+ return (
+
+ User Email: {owner.email}
+
+ );
+};
+```
diff --git a/README.md b/README.md
index 4dc0e15..5550ca4 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ API.
- Derived actions
- `immer`, `devtools` and `persist` middlewares
- Full typescript support
+- `react-tracked` support
## Create a store
@@ -40,6 +41,10 @@ import { createStore } from '@udecode/zustood'
const repoStore = createStore('repo')({
name: 'zustood',
stars: 0,
+ owner: {
+ name: 'someone',
+ email: 'someone@xxx.com',
+ },
})
```
@@ -75,6 +80,17 @@ repoStore.use.stars()
We recommend using the global hooks (see below) to support ESLint hook
linting.
+### Tracked Hooks
+
+> Big thanks for [react-tracked](https://github.com/dai-shi/react-tracked)
+
+Use the tracked hooks in React components, no providers needed. Select your
+state and the component will trigger re-renders only if the **accessed property** is changed. Use the `useTracked` method:
+
+```ts
+repoStore.useTracked.owner()
+```
+
### Getters
Don't overuse hooks. If you don't need to subscribe to the state, use
@@ -182,6 +198,9 @@ export const rootStore = {
// Global hook selectors
export const useStore = () => mapValuesKey('use', rootStore);
+// Global tracked hook selectors
+export const useTrackedStore = () => mapValuesKey('useTracked', rootStore);
+
// Global getter selectors
export const store = mapValuesKey('get', rootStore);
@@ -202,7 +221,32 @@ useStore().modal.isOpen()
useStore().repo.middlewares(shallow)
```
-By using `useStore()`, ESLint will correctly lint hook errors.
+### Global tracked hook selectors
+
+```tsx
+// with useTrackStore UserEmail Component will only re-render when accessed property owner.email changed
+const UserEmail = () => {
+ const owner = useTrackedStore().repo.owner()
+ return (
+
+ User Email: {owner.email}
+
+ );
+};
+
+// with useStore UserEmail Component re-render when owner changed, but you can pass equalityFn to avoid it.
+const UserEmail = () => {
+ const owner = useStore().repo.owner()
+ // const owner = useStore().repo.owner((prev, next) => prev.owner.email === next.owner.email)
+ return (
+
+ User Email: {owner.email}
+
+ );
+};
+```
+
+By using `useStore() or useTrackStore()`, ESLint will correctly lint hook errors.
### Global getter selectors
diff --git a/packages/zustood/package.json b/packages/zustood/package.json
index 5276297..762da37 100644
--- a/packages/zustood/package.json
+++ b/packages/zustood/package.json
@@ -30,7 +30,8 @@
"test": "jest"
},
"dependencies": {
- "immer": "^9.0.6"
+ "immer": "^9.0.6",
+ "react-tracked": "^1.7.9"
},
"peerDependencies": {
"zustand": ">=3.5.10"
diff --git a/packages/zustood/src/createStore.ts b/packages/zustood/src/createStore.ts
index 4d07b5d..7dde13c 100644
--- a/packages/zustood/src/createStore.ts
+++ b/packages/zustood/src/createStore.ts
@@ -1,4 +1,5 @@
import { setAutoFreeze, enableMapSet } from 'immer';
+import { createTrackedSelector } from 'react-tracked';
import create, { State, StateCreator } from 'zustand';
import {
devtools as devtoolsMiddleware,
@@ -18,6 +19,7 @@ import { generateStateActions } from './utils/generateStateActions';
import { storeFactory } from './utils/storeFactory';
import { generateStateGetSelectors } from './utils/generateStateGetSelectors';
import { generateStateHookSelectors } from './utils/generateStateHookSelectors';
+import { generateStateTrackedHooksSelectors } from './utils/generateStateTrackedHooksSelectors';
import { immerMiddleware } from './middlewares/immer.middleware';
import { pipe } from './utils/pipe';
import { CreateStoreOptions } from './types/CreateStoreOptions';
@@ -77,6 +79,12 @@ export const createStore =
const hookSelectors = generateStateHookSelectors(useStore);
const getterSelectors = generateStateGetSelectors(useStore);
+ const useTrackedStore = createTrackedSelector(useStore);
+ const trackedHooksSelectors = generateStateTrackedHooksSelectors(
+ useStore,
+ useTrackedStore
+ );
+
const api = {
get: {
state: store.getState,
@@ -90,7 +98,9 @@ export const createStore =
} as StateActions,
store,
use: hookSelectors,
+ useTracked: trackedHooksSelectors,
useStore,
+ useTrackedStore,
extendSelectors: () => api as any,
extendActions: () => api as any,
};
diff --git a/packages/zustood/src/types.ts b/packages/zustood/src/types.ts
index 3385cb3..2aab75f 100644
--- a/packages/zustood/src/types.ts
+++ b/packages/zustood/src/types.ts
@@ -9,6 +9,10 @@ export type StoreApiGet<
> = StateGetters & TSelectors;
export type StoreApiUse = GetRecord &
TSelectors;
+export type StoreApiUseTracked<
+ T extends State = {},
+ TSelectors = {}
+> = GetRecord & TSelectors;
export type StoreApiSet = TActions;
export type StoreApi<
@@ -22,7 +26,9 @@ export type StoreApi<
set: StoreApiSet;
store: ImmerStoreApi;
use: StoreApiUse;
+ useTracked: StoreApiUseTracked;
useStore: UseImmerStore;
+ useTrackedStore: () => T;
extendSelectors>(
builder: SB
diff --git a/packages/zustood/src/utils/extendSelectors.ts b/packages/zustood/src/utils/extendSelectors.ts
index a9b3122..8eb066c 100644
--- a/packages/zustood/src/utils/extendSelectors.ts
+++ b/packages/zustood/src/utils/extendSelectors.ts
@@ -5,6 +5,7 @@ import {
StoreApi,
StoreApiGet,
StoreApiUse,
+ StoreApiUseTracked,
} from '../types';
export const extendSelectors = <
@@ -26,6 +27,10 @@ export const extendSelectors = <
...api.use,
} as StoreApiUse>;
+ const useTracked = {
+ ...api.useTracked,
+ } as StoreApiUseTracked>;
+
const get = {
...api.get,
} as StoreApiGet>;
@@ -35,6 +40,9 @@ export const extendSelectors = <
use[key] = (...args: any[]) =>
api.useStore((state) => builder(state, api.get, api)[key])(...args);
// @ts-ignore
+ useTracked[key] = (...args: any[]) =>
+ api.useStore((state) => builder(state, api.get, api)[key])(...args);
+ // @ts-ignore
get[key] = (...args: any[]) =>
builder(api.store.getState(), api.get, api)[key](...args);
});
diff --git a/packages/zustood/src/utils/generateStateTrackedHooksSelectors.ts b/packages/zustood/src/utils/generateStateTrackedHooksSelectors.ts
new file mode 100644
index 0000000..883420c
--- /dev/null
+++ b/packages/zustood/src/utils/generateStateTrackedHooksSelectors.ts
@@ -0,0 +1,17 @@
+import { State } from 'zustand';
+import { GetRecord, UseImmerStore } from '../types';
+
+export const generateStateTrackedHooksSelectors = (
+ store: UseImmerStore,
+ trackedStore: () => T
+) => {
+ const selectors: GetRecord = {} as any;
+
+ Object.keys(store.getState()).forEach((key) => {
+ selectors[key] = () => {
+ return trackedStore()[key as keyof T];
+ };
+ });
+
+ return selectors;
+};
diff --git a/yarn.lock b/yarn.lock
index 04fff44..630104b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3585,6 +3585,7 @@ __metadata:
resolution: "@udecode/zustood@workspace:packages/zustood"
dependencies:
immer: ^9.0.6
+ react-tracked: ^1.7.9
peerDependencies:
zustand: ">=3.5.10"
languageName: unknown
@@ -11502,6 +11503,13 @@ __metadata:
languageName: node
linkType: hard
+"proxy-compare@npm:2.1.0":
+ version: 2.1.0
+ resolution: "proxy-compare@npm:2.1.0"
+ checksum: e431403abbb52468045635f434846c55b388c3ccf4012efe729e3fa846513b5d49e2488328582d5be792e7b9b0ba5f5a111887b3a2c4f9a273fc432ab79c7b63
+ languageName: node
+ linkType: hard
+
"prr@npm:~0.0.0":
version: 0.0.0
resolution: "prr@npm:0.0.0"
@@ -11721,6 +11729,26 @@ __metadata:
languageName: node
linkType: hard
+"react-tracked@npm:^1.7.9":
+ version: 1.7.9
+ resolution: "react-tracked@npm:1.7.9"
+ dependencies:
+ proxy-compare: 2.1.0
+ use-context-selector: 1.3.10
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: "*"
+ react-native: "*"
+ scheduler: ">=0.19.0"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ checksum: f8b173603173fd764263fc7e5db304482169faed32a2d528d4414d9ec60dce58c7bba0eba617f91e2e6a6af12900c4b8eb49c2caa1f7e0ac1f848f35db01b15c
+ languageName: node
+ linkType: hard
+
"react@npm:^17.0.1":
version: 17.0.2
resolution: "react@npm:17.0.2"
@@ -14131,6 +14159,23 @@ typescript@^4.4.3:
languageName: node
linkType: hard
+"use-context-selector@npm:1.3.10":
+ version: 1.3.10
+ resolution: "use-context-selector@npm:1.3.10"
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: "*"
+ react-native: "*"
+ scheduler: ">=0.19.0"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ checksum: 86fe17bb25dc2c6730e83911b3baf46fbc1be45f27c86bf2f94b4ed55d443572c6a5d725b77cf6a2ae1ddfe388bfbd6850851591ca1a26e7e216e5aa58b2631d
+ languageName: node
+ linkType: hard
+
"use@npm:^3.1.0":
version: 3.1.1
resolution: "use@npm:3.1.1"