Skip to content

Commit

Permalink
Merge pull request #36 from ShinyLeee/main
Browse files Browse the repository at this point in the history
[Feat] add tracked hooks selector (big thanks for react-tracked)
  • Loading branch information
zbeyens authored Apr 22, 2022
2 parents 1a985db + c66963c commit 340618e
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 2 deletions.
33 changes: 33 additions & 0 deletions .changeset/witty-games-raise.md
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<span>User Email: {owner.email}</span>
</div>
);
};
// 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 (
<div>
<span>User Email: {owner.email}</span>
</div>
);
};
```
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ API.
- Derived actions
- `immer`, `devtools` and `persist` middlewares
- Full typescript support
- `react-tracked` support

## Create a store

Expand All @@ -40,6 +41,10 @@ import { createStore } from '@udecode/zustood'
const repoStore = createStore('repo')({
name: 'zustood',
stars: 0,
owner: {
name: 'someone',
email: '[email protected]',
},
})
```

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand All @@ -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 (
<div>
<span>User Email: {owner.email}</span>
</div>
);
};

// 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 (
<div>
<span>User Email: {owner.email}</span>
</div>
);
};
```

By using `useStore() or useTrackStore()`, ESLint will correctly lint hook errors.

### Global getter selectors

Expand Down
3 changes: 2 additions & 1 deletion packages/zustood/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions packages/zustood/src/createStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { setAutoFreeze, enableMapSet } from 'immer';
import { createTrackedSelector } from 'react-tracked';
import create, { State, StateCreator } from 'zustand';
import {
devtools as devtoolsMiddleware,
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -90,7 +98,9 @@ export const createStore =
} as StateActions<T>,
store,
use: hookSelectors,
useTracked: trackedHooksSelectors,
useStore,
useTrackedStore,
extendSelectors: () => api as any,
extendActions: () => api as any,
};
Expand Down
6 changes: 6 additions & 0 deletions packages/zustood/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export type StoreApiGet<
> = StateGetters<T> & TSelectors;
export type StoreApiUse<T extends State = {}, TSelectors = {}> = GetRecord<T> &
TSelectors;
export type StoreApiUseTracked<
T extends State = {},
TSelectors = {}
> = GetRecord<T> & TSelectors;
export type StoreApiSet<TActions = {}> = TActions;

export type StoreApi<
Expand All @@ -22,7 +26,9 @@ export type StoreApi<
set: StoreApiSet<TActions>;
store: ImmerStoreApi<T>;
use: StoreApiUse<T, TSelectors>;
useTracked: StoreApiUseTracked<T, TSelectors>;
useStore: UseImmerStore<T>;
useTrackedStore: () => T;

extendSelectors<SB extends SelectorBuilder<TName, T, TActions, TSelectors>>(
builder: SB
Expand Down
8 changes: 8 additions & 0 deletions packages/zustood/src/utils/extendSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
StoreApi,
StoreApiGet,
StoreApiUse,
StoreApiUseTracked,
} from '../types';

export const extendSelectors = <
Expand All @@ -26,6 +27,10 @@ export const extendSelectors = <
...api.use,
} as StoreApiUse<T, TSelectors & ReturnType<CB>>;

const useTracked = {
...api.useTracked,
} as StoreApiUseTracked<T, TSelectors & ReturnType<CB>>;

const get = {
...api.get,
} as StoreApiGet<T, TSelectors & ReturnType<CB>>;
Expand All @@ -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);
});
Expand Down
17 changes: 17 additions & 0 deletions packages/zustood/src/utils/generateStateTrackedHooksSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { State } from 'zustand';
import { GetRecord, UseImmerStore } from '../types';

export const generateStateTrackedHooksSelectors = <T extends State>(
store: UseImmerStore<T>,
trackedStore: () => T
) => {
const selectors: GetRecord<T> = {} as any;

Object.keys(store.getState()).forEach((key) => {
selectors[key] = () => {
return trackedStore()[key as keyof T];
};
});

return selectors;
};
45 changes: 45 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 340618e

Please sign in to comment.